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.
977 lines
32 KiB
977 lines
32 KiB
import 'dart:async';
|
|
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
|
|
import 'package:audioplayers/audioplayers.dart';
|
|
import 'package:dating_touchme_app/generated/assets.dart';
|
|
import 'package:dating_touchme_app/model/rtc/chat_audio_product_model.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:flutter/cupertino.dart';
|
|
import 'package:flutter/material.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 '../../widget/live/live_recharge_popup.dart';
|
|
import '../discover/room_controller.dart';
|
|
import '../overlay_controller.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 {
|
|
if (_instance != null) {
|
|
return _instance!;
|
|
}
|
|
// 如果 GetX 中已注册,使用 Get.find 获取
|
|
if (Get.isRegistered<CallController>()) {
|
|
_instance = Get.find<CallController>();
|
|
return _instance!;
|
|
}
|
|
// 否则创建新实例并注册
|
|
_instance = Get.put(CallController(), permanent: true);
|
|
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);
|
|
|
|
// 通话超时计时器(发起方等待对方接听的30秒超时)
|
|
Timer? _callTimeoutTimer;
|
|
|
|
// 远端用户UID(用于显示远端视频)
|
|
final Rxn<int> remoteUid = Rxn<int>();
|
|
|
|
// 麦克风静音状态
|
|
final RxBool isMicMuted = false.obs;
|
|
|
|
// 扬声器开启状态
|
|
final RxBool isSpeakerOn = false.obs;
|
|
|
|
// 音频播放器(用于播放来电铃声)
|
|
final AudioPlayer _callAudioPlayer = AudioPlayer();
|
|
bool _isPlayingCallAudio = false;
|
|
|
|
// 当前通话的频道ID和UID(用于发送RTM消息)
|
|
String? _callChannelId;
|
|
int? _callUid;
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
print('📞 [CallController] 通话控制器已初始化');
|
|
// 监听音频播放完成事件,实现循环播放
|
|
_callAudioPlayer.onPlayerComplete.listen((_) async {
|
|
if (_isPlayingCallAudio) {
|
|
// 如果还在播放状态,重新播放(循环播放)
|
|
await _callAudioPlayer.play(
|
|
AssetSource(Assets.audioCall.replaceFirst('assets/', '')),
|
|
);
|
|
}
|
|
});
|
|
|
|
// RTM 消息监听器已移除,通话相关消息改为通过onMessageContentChanged处理
|
|
}
|
|
|
|
/// 创建一对一RTC频道
|
|
/// [type] 1为音频,2为视频
|
|
Future<RtcChannelData?> createOneOnOneRtcChannel({
|
|
required int type,
|
|
required String toUserId,
|
|
}) async {
|
|
if (isCreatingChannel.value) {
|
|
print('⚠️ 正在创建频道,请稍候');
|
|
return null;
|
|
}
|
|
|
|
// 验证 type 参数
|
|
if (type != 2 && type != 3) {
|
|
SmartDialog.showToast('类型参数错误:1为音频,2为视频');
|
|
return null;
|
|
}
|
|
|
|
// 检查权限:语音通话需要麦克风,视频通话需要摄像头和麦克风
|
|
final hasPermission = await _ensureCallPermissions(type);
|
|
if (!hasPermission) {
|
|
print('❌ [CallController] 权限检查失败,无法创建通话频道');
|
|
return null;
|
|
}
|
|
|
|
isCreatingChannel.value = true;
|
|
|
|
final response = await _networkService.rtcApi.createOneOnOneRtcChannel({
|
|
'type': type,
|
|
'toUserId': toUserId,
|
|
});
|
|
if (response.data.isSuccess && response.data.data != null) {
|
|
if (!response.data.data!.success && response.data.data!.code == 'E0002') {
|
|
// 玫瑰不足,显示 toast 并弹出充值弹框
|
|
SmartDialog.showToast('玫瑰不足请充值');
|
|
Get.log('❌ 送礼失败: ${response.data.data}');
|
|
// 使用 addPostFrameCallback 确保在下一帧显示弹框,避免与 toast 冲突
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
SmartDialog.show(
|
|
alignment: Alignment.bottomCenter,
|
|
maskColor: Colors.black.withOpacity(0.5),
|
|
builder: (_) => const LiveRechargePopup(),
|
|
);
|
|
});
|
|
return 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;
|
|
}
|
|
}
|
|
|
|
/// 获取聊天音频产品列表
|
|
/// [toUserId] 目标用户ID
|
|
Future<List<ChatAudioProductModel>?> listChatAudioProduct(String toUserId) async {
|
|
try {
|
|
final response = await _networkService.rtcApi.listChatAudioProduct(toUserId);
|
|
if (response.data.isSuccess) {
|
|
print('✅ [CallController] 获取聊天音频产品列表成功');
|
|
return response.data.data;
|
|
} else {
|
|
final message = response.data.message.isNotEmpty
|
|
? response.data.message
|
|
: '获取聊天音频产品列表失败';
|
|
SmartDialog.showToast(message);
|
|
print('❌ [CallController] 获取聊天音频产品列表失败: $message');
|
|
return null;
|
|
}
|
|
} catch (e) {
|
|
SmartDialog.showToast('网络请求失败,请重试');
|
|
print('❌ [CallController] 获取聊天音频产品列表异常: $e');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// 发起通话
|
|
/// [targetUserId] 目标用户ID
|
|
/// [callType] 通话类型:语音或视频
|
|
/// [chatController] 聊天控制器,用于发送通话消息
|
|
Future<bool> initiateCall({
|
|
required String targetUserId,
|
|
required CallType callType,
|
|
ChatController? chatController,
|
|
}) async {
|
|
if (currentCall.value != null) {
|
|
SmartDialog.showToast('当前正在通话中');
|
|
return false;
|
|
}
|
|
|
|
// 检查是否在直播间
|
|
try {
|
|
if (Get.isRegistered<RoomController>()) {
|
|
final roomController = Get.find<RoomController>();
|
|
if (roomController.isLive.value) {
|
|
SmartDialog.showToast('请先退出直播间');
|
|
print('⚠️ [CallController] 当前在直播间,无法发起通话');
|
|
return false;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// 忽略错误,继续处理发起通话逻辑
|
|
print('⚠️ [CallController] 检查直播间状态失败: $e');
|
|
}
|
|
|
|
// 清空之前的远端用户UID和通话信息
|
|
remoteUid.value = null;
|
|
|
|
print(
|
|
'📞 [CallController] 发起${callType == CallType.video ? "视频" : "语音"}通话,目标用户: $targetUserId',
|
|
);
|
|
|
|
// 发起通话前,先创建一对一 RTC 频道
|
|
final type = callType == CallType.video ? 3 : 2; // 1为音频,2为视频
|
|
final channelData = await createOneOnOneRtcChannel(
|
|
type: type,
|
|
toUserId: targetUserId,
|
|
);
|
|
_callUid = channelData?.uid;
|
|
_callChannelId = channelData?.channelId;
|
|
if (channelData == null) {
|
|
print('❌ [CallController] 创建RTC频道失败,无法发起通话');
|
|
SmartDialog.showToast('创建通话频道失败');
|
|
return false;
|
|
}
|
|
|
|
print('✅ [CallController] RTC频道创建成功: ${channelData.channelId}');
|
|
|
|
// 创建通话会话
|
|
final session = CallSession(
|
|
targetUserId: targetUserId,
|
|
callType: callType,
|
|
status: CallStatus.waitCalling,
|
|
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,
|
|
uid: channelData.uid,
|
|
);
|
|
|
|
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}');
|
|
|
|
// 启动30秒超时计时器(如果30秒内对方未接听,自动取消通话)
|
|
_startCallTimeoutTimer();
|
|
|
|
return true;
|
|
}
|
|
|
|
/// 接听通话
|
|
/// [message] 通话消息
|
|
/// [chatController] 聊天控制器,用于更新通话消息
|
|
Future<bool> acceptCall({
|
|
required EMMessage message,
|
|
ChatController? chatController,
|
|
}) async {
|
|
// 检查是否在直播间
|
|
try {
|
|
if (Get.isRegistered<RoomController>()) {
|
|
final roomController = Get.find<RoomController>();
|
|
if (roomController.isLive.value) {
|
|
SmartDialog.showToast('请先退出直播间再接听');
|
|
print('⚠️ [CallController] 当前在直播间,无法接听通话');
|
|
return false;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// 忽略错误,继续处理接听逻辑
|
|
print('⚠️ [CallController] 检查直播间状态失败: $e');
|
|
}
|
|
|
|
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;
|
|
_callChannelId = null;
|
|
_callUid = null;
|
|
|
|
// 创建通话会话
|
|
final session = CallSession(
|
|
targetUserId: targetUserId,
|
|
callType: callType,
|
|
status: CallStatus.calling,
|
|
isInitiator: false,
|
|
startTime: DateTime.now(),
|
|
);
|
|
currentCall.value = session;
|
|
|
|
// 停止播放来电铃声(已接通)
|
|
stopCallAudio();
|
|
|
|
// 开始计时
|
|
_startCallTimer();
|
|
|
|
// 从通话信息中获取 channelId 和 uid
|
|
final channelId = callInfo['channelId'] as String?;
|
|
if (channelId == null || channelId.isEmpty) {
|
|
print('❌ [CallController] channelId 为空,无法加入 RTC 频道');
|
|
SmartDialog.showToast('频道ID不存在');
|
|
return false;
|
|
}
|
|
|
|
// 保存 channelId 为全局变量
|
|
_callChannelId = channelId;
|
|
|
|
// 调用连接一对一RTC频道接口
|
|
final connectResponse = await _networkService.rtcApi
|
|
.connectOneOnOneRtcChannel({'channelId': channelId});
|
|
if (!connectResponse.data.isSuccess) {
|
|
SmartDialog.showToast(connectResponse.data.message);
|
|
return false;
|
|
}
|
|
if (!connectResponse.data.data!['success'] && connectResponse.data.data!['code'] == 'E0002') {
|
|
// 玫瑰不足,显示 toast 并弹出充值弹框
|
|
SmartDialog.showToast('玫瑰不足请充值');
|
|
// 使用 addPostFrameCallback 确保在下一帧显示弹框,避免与 toast 冲突
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
SmartDialog.show(
|
|
alignment: Alignment.bottomCenter,
|
|
maskColor: Colors.black.withOpacity(0.5),
|
|
builder: (_) => const LiveRechargePopup(),
|
|
);
|
|
});
|
|
return false;
|
|
}
|
|
print('✅ [CallController] 已调用连接一对一RTC频道接口,channelId: $channelId');
|
|
|
|
// 根据通话类型设置摄像头状态
|
|
if (callType == CallType.voice) {
|
|
// 语音通话:禁用视频(关闭摄像头)
|
|
await RTCManager.instance.disableVideo();
|
|
print('📞 [CallController] 语音通话,已关闭摄像头');
|
|
} else {
|
|
// 视频通话:启用视频(打开摄像头)
|
|
await RTCManager.instance.enableVideo();
|
|
print('📞 [CallController] 视频通话,已打开摄像头');
|
|
}
|
|
|
|
// 获取 RTC token 并加入频道
|
|
final response = await _networkService.rtcApi.getSwRtcToken(channelId);
|
|
final base = response.data;
|
|
if (base.isSuccess && base.data != null) {
|
|
rtcChannel.value = base.data;
|
|
|
|
// 保存 UID 为全局变量
|
|
_callUid = base.data!.uid;
|
|
|
|
await _joinRtcChannel(
|
|
base.data!.token,
|
|
channelId,
|
|
base.data!.uid,
|
|
ClientRoleType.clientRoleBroadcaster,
|
|
);
|
|
|
|
// 从消息中获取发起方的 uid,设置为远端用户 UID
|
|
final initiatorUid = callInfo['uid'] as int?;
|
|
if (initiatorUid != null) {
|
|
remoteUid.value = initiatorUid;
|
|
print(
|
|
'📞 [CallController] 从消息中获取到发起方 UID: $initiatorUid,已设置 remoteUid',
|
|
);
|
|
}
|
|
} else {
|
|
SmartDialog.showToast('获取RTC token失败');
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// 拒绝通话
|
|
/// [message] 通话消息
|
|
/// [chatController] 聊天控制器,用于更新通话消息
|
|
Future<bool> rejectCall({
|
|
required EMMessage message,
|
|
ChatController? chatController,
|
|
}) async {
|
|
print('📞 [CallController] 拒绝通话');
|
|
|
|
// 停止播放来电铃声(已拒绝)
|
|
stopCallAudio();
|
|
|
|
// 从消息中获取 channelId
|
|
String? channelId;
|
|
if (message.body is EMCustomMessageBody) {
|
|
final customBody = message.body as EMCustomMessageBody;
|
|
final params = customBody.params;
|
|
if (params != null && params.containsKey('channelId')) {
|
|
channelId = params['channelId']?.toString();
|
|
}
|
|
}
|
|
|
|
// 如果有 channelId,调用拒绝接口
|
|
if (channelId != null && channelId.isNotEmpty) {
|
|
final response = await _networkService.rtcApi.refuseOneOnOneRtcChannel({
|
|
'channelId': channelId,
|
|
});
|
|
if (!response.data.isSuccess) {
|
|
SmartDialog.showToast(response.data.message);
|
|
return false;
|
|
}
|
|
print('✅ [CallController] 已调用拒绝一对一RTC频道接口,channelId: $channelId');
|
|
// 服务端会自动修改消息callStatus为'rejected',客户端通过onMessageContentChanged收到通知
|
|
}
|
|
|
|
// 清理通话会话
|
|
currentCall.value = null;
|
|
|
|
return true;
|
|
}
|
|
|
|
/// 结束通话(通话完成)
|
|
/// [callDuration] 通话时长(秒)
|
|
/// [chatController] 聊天控制器,用于更新通话消息
|
|
Future<bool> endCall({
|
|
required int callDuration,
|
|
EMMessage? message,
|
|
ChatController? chatController,
|
|
}) async {
|
|
print('📞 [CallController] 结束通话,时长: ${callDuration}秒');
|
|
|
|
// 停止播放来电铃声(通话结束)
|
|
stopCallAudio();
|
|
|
|
// 停止计时和超时计时器
|
|
_stopCallTimer();
|
|
_stopCallTimeoutTimer();
|
|
|
|
// 清理通话会话和远端用户UID
|
|
currentCall.value = null;
|
|
remoteUid.value = null;
|
|
|
|
// 清理保存的 channelId 和 uid
|
|
_callChannelId = null;
|
|
_callUid = 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;
|
|
}
|
|
|
|
/// 启动通话超时计时器(30秒后如果对方未接听,自动取消通话)
|
|
void _startCallTimeoutTimer() {
|
|
_callTimeoutTimer?.cancel();
|
|
_callTimeoutTimer = Timer(Duration(seconds: 30), () {
|
|
final callSession = currentCall.value;
|
|
// 如果是发起方且对方未接听(通话时长仍为0且remoteUid为null),自动取消通话
|
|
if (callSession != null &&
|
|
callSession.isInitiator &&
|
|
callDurationSeconds.value == 0 &&
|
|
remoteUid.value == null) {
|
|
print('⏰ [CallController] 30秒超时,对方未接听,自动取消通话');
|
|
hangUpCall();
|
|
}
|
|
});
|
|
}
|
|
|
|
/// 停止通话超时计时器
|
|
void _stopCallTimeoutTimer() {
|
|
_callTimeoutTimer?.cancel();
|
|
_callTimeoutTimer = null;
|
|
}
|
|
|
|
/// 发送通话消息
|
|
Future<bool> _sendCallMessage({
|
|
required String targetUserId,
|
|
required String callType,
|
|
required String callStatus,
|
|
int? callDuration,
|
|
int? uid,
|
|
String? channelId,
|
|
ChatController? chatController,
|
|
}) async {
|
|
// 如果提供了 chatController,使用它发送消息
|
|
return await chatController!.sendCallMessage(
|
|
callType: callType,
|
|
callStatus: callStatus,
|
|
callDuration: callDuration,
|
|
channelId: channelId,
|
|
uid: uid,
|
|
);
|
|
}
|
|
|
|
/// 从自定义消息中解析通话信息
|
|
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'],
|
|
'uid': params['uid'] != null ? int.tryParse(params['uid']!) : null,
|
|
};
|
|
}
|
|
}
|
|
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;
|
|
|
|
// 保存 UID 为全局变量
|
|
_callUid = base.data!.uid;
|
|
|
|
await _joinRtcChannel(
|
|
base.data!.token,
|
|
channelName,
|
|
base.data!.uid,
|
|
ClientRoleType.clientRoleBroadcaster,
|
|
);
|
|
// 服务端会自动修改消息callStatus为'calling',客户端通过onMessageContentChanged收到通知
|
|
}
|
|
}
|
|
|
|
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,
|
|
);
|
|
print('✅ [CallController] 已加入 RTC 频道: $channelName');
|
|
}
|
|
|
|
/// 检查通话权限
|
|
/// [type] 2=语音通话,3=视频通话
|
|
Future<bool> _ensureCallPermissions(int type) async {
|
|
if (type == 2) {
|
|
// 语音通话:只需要麦克风权限
|
|
final micStatus = await Permission.microphone.request();
|
|
if (micStatus.isGranted) {
|
|
return true;
|
|
}
|
|
|
|
if (micStatus.isPermanentlyDenied) {
|
|
SmartDialog.showToast('请在系统设置中开启麦克风权限');
|
|
await openAppSettings();
|
|
} else {
|
|
SmartDialog.showToast('请允许麦克风权限以进行语音通话');
|
|
}
|
|
return false;
|
|
} else if (type == 3) {
|
|
// 视频通话:需要摄像头和麦克风权限
|
|
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;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
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 {
|
|
final callSession = currentCall.value;
|
|
|
|
// 取消超时计时器
|
|
_stopCallTimeoutTimer();
|
|
|
|
// 如果是发起方且处于呼叫中状态(对方还没接听),先调用取消接口
|
|
if (callSession != null &&
|
|
callSession.isInitiator &&
|
|
callDurationSeconds.value == 0 &&
|
|
_callChannelId != null &&
|
|
_callChannelId!.isNotEmpty) {
|
|
try {
|
|
final response = await _networkService.rtcApi.cancelOneOnOneRtcChannel({
|
|
'channelId': _callChannelId!,
|
|
});
|
|
if (response.data.isSuccess) {
|
|
print('✅ [CallController] 已调用取消一对一RTC频道接口,channelId: $_callChannelId');
|
|
} else {
|
|
print('⚠️ [CallController] 取消一对一RTC频道接口失败: ${response.data.message}');
|
|
}
|
|
} catch (e) {
|
|
print('⚠️ [CallController] 调用取消接口异常: $e');
|
|
}
|
|
} else if (callSession != null &&
|
|
callDurationSeconds.value > 0 &&
|
|
_callChannelId != null &&
|
|
_callChannelId!.isNotEmpty) {
|
|
// 如果通话已接通(无论是发起方还是接收方),调用终止接口
|
|
try {
|
|
final response = await _networkService.rtcApi.terminateOneOnOneRtcChannel(
|
|
{'channelId': _callChannelId!},
|
|
);
|
|
if (response.data.isSuccess) {
|
|
print('✅ [CallController] 已调用终止一对一RTC频道接口,channelId: $_callChannelId');
|
|
} else {
|
|
print('⚠️ [CallController] 终止一对一RTC频道接口失败: ${response.data.message}');
|
|
}
|
|
} catch (e) {
|
|
print('⚠️ [CallController] 调用终止接口异常: $e');
|
|
}
|
|
}
|
|
|
|
// 离开RTC频道
|
|
try {
|
|
await RTCManager.instance.leaveChannel();
|
|
} catch (e) {
|
|
print('⚠️ [CallController] 离开RTC频道异常: $e');
|
|
}
|
|
|
|
// 服务端会自动修改消息callStatus为'cancelled'或'terminated',客户端通过onMessageContentChanged收到通知
|
|
|
|
// 结束通话(传递通话时长)
|
|
await endCall(callDuration: callDurationSeconds.value);
|
|
|
|
// 关闭视频通话邀请弹框(如果正在显示)
|
|
SmartDialog.dismiss();
|
|
|
|
// 关闭通话小窗口
|
|
if (Get.isRegistered<OverlayController>()) {
|
|
final overlayController = Get.find<OverlayController>();
|
|
overlayController.hideVideoCall();
|
|
print('✅ [CallController] 已关闭通话小窗口');
|
|
}
|
|
|
|
// 退出 VideoCallPage(如果当前在 VideoCallPage)
|
|
if (Get.currentRoute.contains('VideoCallPage')) {
|
|
Get.back();
|
|
print('✅ [CallController] 已退出 VideoCallPage');
|
|
}
|
|
|
|
print('✅ [CallController] 通话已挂断');
|
|
}
|
|
|
|
/// 处理通话消息callStatus变化(通过onMessageContentChanged调用)
|
|
Future<void> handleCallStatusChange({
|
|
required EMMessage message,
|
|
required String callStatus,
|
|
String? channelId,
|
|
int? uid,
|
|
int? callDuration,
|
|
}) async {
|
|
print(
|
|
'📞 [CallController] 处理callStatus变化: callStatus=$callStatus, channelId=$channelId',
|
|
);
|
|
|
|
// 对于 cancelled 和 terminated 状态,即使没有 currentCall,也要处理(关闭小窗口等)
|
|
if (callStatus == 'cancelled' || callStatus == 'terminated') {
|
|
final callSession = currentCall.value;
|
|
// 如果有 channelId,验证是否匹配当前通话(如果存在)
|
|
if (callSession != null && channelId != null && channelId.isNotEmpty && _callChannelId != channelId) {
|
|
print(
|
|
'⚠️ [CallController] channelId不匹配,忽略callStatus变化: 当前=$_callChannelId, 消息=$channelId',
|
|
);
|
|
return;
|
|
}
|
|
|
|
// 处理取消/终止状态(即使没有 callSession 也要关闭小窗口)
|
|
print(
|
|
'📞 [CallController] 通话被取消/终止,callStatus=$callStatus, hasCallSession=${callSession != null}',
|
|
);
|
|
|
|
// 关闭视频通话邀请弹框(如果正在显示)
|
|
SmartDialog.dismiss();
|
|
print('✅ [CallController] 已关闭视频通话邀请弹框');
|
|
|
|
// 取消超时计时器
|
|
_stopCallTimeoutTimer();
|
|
|
|
// 停止播放来电铃声
|
|
stopCallAudio();
|
|
|
|
// 停止通话计时器
|
|
_stopCallTimer();
|
|
|
|
// 离开RTC频道
|
|
await RTCManager.instance.leaveChannel();
|
|
|
|
// 如果有通话会话,结束通话
|
|
if (callSession != null) {
|
|
await endCall(callDuration: callDuration ?? callDurationSeconds.value);
|
|
}
|
|
|
|
// 关闭通话小窗口
|
|
if (Get.isRegistered<OverlayController>()) {
|
|
final overlayController = Get.find<OverlayController>();
|
|
overlayController.hideVideoCall();
|
|
print('✅ [CallController] 已关闭通话小窗口');
|
|
}
|
|
|
|
// 退出 VideoCallPage(如果当前在 VideoCallPage)
|
|
if (Get.currentRoute.contains('VideoCallPage')) {
|
|
Get.back();
|
|
print('✅ [CallController] 已退出 VideoCallPage');
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// 对于其他状态,需要检查是否有进行中的通话
|
|
final callSession = currentCall.value;
|
|
if (callSession == null) {
|
|
print('⚠️ [CallController] 当前没有进行中的通话,忽略callStatus变化');
|
|
return;
|
|
}
|
|
|
|
// 如果提供了channelId,验证是否匹配当前通话
|
|
if (channelId != null &&
|
|
channelId.isNotEmpty &&
|
|
_callChannelId != channelId) {
|
|
print(
|
|
'⚠️ [CallController] channelId不匹配,忽略callStatus变化: 当前=$_callChannelId, 消息=$channelId',
|
|
);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (callStatus == 'calling') {
|
|
// 通话接通
|
|
print('📞 [CallController] 通话已接通,callStatus=$callStatus');
|
|
|
|
// 如果是发起方,设置远端用户UID并启动计时器
|
|
if (callSession.isInitiator) {
|
|
// 停止播放来电铃声(对方已接听)
|
|
stopCallAudio();
|
|
// 取消超时计时器(对方已接听)
|
|
_stopCallTimeoutTimer();
|
|
// 启动通话计时器
|
|
_startCallTimer();
|
|
print('📞 [CallController] 已启动通话计时器');
|
|
// remoteUid 会由 RTCManager 的 onUserJoined 事件自动设置
|
|
// 如果此时还没有设置,尝试从 RTCManager 的 remoteUsersNotifier 中获取
|
|
if (remoteUid.value == null) {
|
|
final rtcManager = RTCManager.instance;
|
|
final remoteUsers = rtcManager.remoteUsersNotifier.value;
|
|
if (remoteUsers.isNotEmpty) {
|
|
remoteUid.value = remoteUsers.first;
|
|
print(
|
|
'📞 [CallController] 从 RTCManager.remoteUsersNotifier 获取到 remoteUid: ${remoteUsers.first}',
|
|
);
|
|
} else if (uid != null) {
|
|
// 如果 RTCManager 还没有远端用户,使用消息中的 uid 作为备用
|
|
remoteUid.value = uid;
|
|
print('📞 [CallController] 使用消息中的 uid 设置 remoteUid: $uid');
|
|
}
|
|
}
|
|
}
|
|
} else if (callStatus == 'rejected') {
|
|
// 通话被拒绝
|
|
print('📞 [CallController] 通话被拒绝,callStatus=$callStatus');
|
|
|
|
// 如果是发起方,执行退出逻辑
|
|
if (callSession.isInitiator) {
|
|
// 取消超时计时器(对方已拒绝)
|
|
_stopCallTimeoutTimer();
|
|
// 关闭通话小窗口
|
|
if (Get.isRegistered<OverlayController>()) {
|
|
final overlayController = Get.find<OverlayController>();
|
|
overlayController.hideVideoCall();
|
|
print('✅ [CallController] 已关闭通话小窗口');
|
|
}
|
|
// 退出 VideoCallPage(如果当前在 VideoCallPage)
|
|
if (Get.currentRoute.contains('VideoCallPage')) {
|
|
Get.back();
|
|
print('✅ [CallController] 已退出 VideoCallPage');
|
|
}
|
|
// 结束通话
|
|
await endCall(callDuration: callDurationSeconds.value);
|
|
}
|
|
}
|
|
// 注意:cancelled 和 terminated 状态已在方法开头处理,这里不再重复处理
|
|
} catch (e) {
|
|
print('❌ [CallController] 处理callStatus变化失败: $e');
|
|
}
|
|
}
|
|
|
|
@override
|
|
void onClose() {
|
|
stopCallAudio();
|
|
_stopCallTimer();
|
|
_stopCallTimeoutTimer();
|
|
currentCall.value = null;
|
|
rtcChannel.value = null;
|
|
remoteUid.value = null;
|
|
isMicMuted.value = false;
|
|
isSpeakerOn.value = false;
|
|
_callChannelId = null;
|
|
_callUid = null;
|
|
_callAudioPlayer.dispose();
|
|
super.onClose();
|
|
}
|
|
}
|