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.
678 lines
20 KiB
678 lines
20 KiB
import 'dart:async';
|
|
import 'dart:ui';
|
|
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
|
import 'package:dating_touchme_app/generated/assets.dart';
|
|
import 'package:dating_touchme_app/rtc/rtc_manager.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.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 '../../controller/message/call_controller.dart';
|
|
import '../../controller/message/chat_controller.dart';
|
|
import '../../controller/message/conversation_controller.dart';
|
|
import '../../controller/overlay_controller.dart';
|
|
import '../../model/home/marriage_data.dart';
|
|
|
|
/// 视频通话页面
|
|
class VideoCallPage extends StatefulWidget {
|
|
final String targetUserId;
|
|
final MarriageData? userData;
|
|
final bool isInitiator; // 是否是发起方
|
|
|
|
const VideoCallPage({
|
|
super.key,
|
|
required this.targetUserId,
|
|
this.userData,
|
|
this.isInitiator = true,
|
|
});
|
|
|
|
@override
|
|
State<VideoCallPage> createState() => _VideoCallPageState();
|
|
}
|
|
|
|
class _VideoCallPageState extends State<VideoCallPage> {
|
|
final CallController _callController = CallController.instance;
|
|
final RTCManager _rtcManager = RTCManager.instance;
|
|
|
|
bool _isMicMuted = false;
|
|
bool _isSpeakerOn = false;
|
|
Duration _callDuration = Duration.zero;
|
|
Timer? _durationTimer;
|
|
|
|
String? _targetUserName;
|
|
String? _targetAvatarUrl;
|
|
|
|
// 通话是否已接通
|
|
bool _isCallConnected = false;
|
|
|
|
// 本地视频视图控制器
|
|
VideoViewController? _localVideoViewController;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadUserInfo();
|
|
_initCallStatus();
|
|
_startDurationTimer();
|
|
_initLocalVideo();
|
|
|
|
// 设置系统UI样式
|
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
|
|
SystemChrome.setPreferredOrientations([
|
|
DeviceOrientation.portraitUp,
|
|
]);
|
|
}
|
|
|
|
/// 初始化本地视频视图
|
|
void _initLocalVideo() {
|
|
final callSession = _callController.currentCall.value;
|
|
// 如果是视频通话,创建本地视频视图控制器
|
|
if (callSession != null && callSession.callType == CallType.video) {
|
|
final engine = _rtcManager.engine;
|
|
if (engine != null) {
|
|
_localVideoViewController = VideoViewController(
|
|
rtcEngine: engine,
|
|
canvas: const VideoCanvas(uid: 0),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 初始化通话状态
|
|
void _initCallStatus() {
|
|
// 检查当前通话状态
|
|
final callSession = _callController.currentCall.value;
|
|
if (callSession != null && _callController.callDurationSeconds.value > 0) {
|
|
// 如果通话已存在且已经开始计时,说明已接通
|
|
_isCallConnected = true;
|
|
_callDuration = Duration(seconds: _callController.callDurationSeconds.value);
|
|
} else {
|
|
// 否则是未接通状态
|
|
_isCallConnected = false;
|
|
}
|
|
}
|
|
|
|
/// 加载用户信息
|
|
Future<void> _loadUserInfo() async {
|
|
// 优先使用传入的 userData
|
|
if (widget.userData != null) {
|
|
setState(() {
|
|
_targetUserName = widget.userData!.nickName.isNotEmpty
|
|
? widget.userData!.nickName
|
|
: widget.targetUserId;
|
|
_targetAvatarUrl = widget.userData!.profilePhoto;
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 如果没有传入 userData,尝试从 ConversationController 获取
|
|
try {
|
|
if (Get.isRegistered<ConversationController>()) {
|
|
final conversationController = Get.find<ConversationController>();
|
|
|
|
// 先从缓存中获取
|
|
final cachedUserInfo = conversationController.getCachedUserInfo(widget.targetUserId);
|
|
if (cachedUserInfo != null && (cachedUserInfo.nickName != null || cachedUserInfo.avatarUrl != null)) {
|
|
setState(() {
|
|
_targetUserName = cachedUserInfo.nickName ?? widget.targetUserId;
|
|
_targetAvatarUrl = cachedUserInfo.avatarUrl;
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 如果缓存中没有,尝试从 IM 加载
|
|
final userInfo = await conversationController.loadContact(widget.targetUserId);
|
|
if (userInfo != null && (userInfo.nickName != null || userInfo.avatarUrl != null)) {
|
|
setState(() {
|
|
_targetUserName = userInfo.nickName ?? widget.targetUserId;
|
|
_targetAvatarUrl = userInfo.avatarUrl;
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
print('⚠️ [VideoCallPage] 加载用户信息失败: $e');
|
|
}
|
|
|
|
// 如果都获取不到,使用默认值
|
|
setState(() {
|
|
_targetUserName = widget.targetUserId;
|
|
_targetAvatarUrl = null;
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_durationTimer?.cancel();
|
|
_localVideoViewController?.dispose();
|
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values);
|
|
SystemChrome.setPreferredOrientations(DeviceOrientation.values);
|
|
super.dispose();
|
|
}
|
|
|
|
/// 开始通话时长计时器
|
|
void _startDurationTimer() {
|
|
// 监听 CallController 的通话状态变化
|
|
_callController.currentCall.listen((callSession) {
|
|
if (mounted) {
|
|
final wasConnected = _isCallConnected;
|
|
// 如果通话存在且已经开始计时,说明已接通
|
|
if (callSession != null && _callController.callDurationSeconds.value > 0) {
|
|
_isCallConnected = true;
|
|
if (!wasConnected) {
|
|
// 刚接通,同步时长
|
|
_callDuration = Duration(seconds: _callController.callDurationSeconds.value);
|
|
}
|
|
} else if (callSession == null) {
|
|
_isCallConnected = false;
|
|
}
|
|
setState(() {});
|
|
}
|
|
});
|
|
|
|
// 监听通话时长变化(已接通时更新)
|
|
_callController.callDurationSeconds.listen((seconds) {
|
|
if (mounted && _isCallConnected) {
|
|
setState(() {
|
|
_callDuration = Duration(seconds: seconds);
|
|
});
|
|
} else if (mounted && !_isCallConnected && seconds > 0) {
|
|
// 如果时长开始增加,说明刚接通
|
|
setState(() {
|
|
_isCallConnected = true;
|
|
_callDuration = Duration(seconds: seconds);
|
|
});
|
|
}
|
|
});
|
|
|
|
// 如果未接通,使用本地计时器检查状态变化
|
|
if (!_isCallConnected) {
|
|
_durationTimer = Timer.periodic(Duration(seconds: 1), (timer) {
|
|
if (mounted) {
|
|
final callSession = _callController.currentCall.value;
|
|
final duration = _callController.callDurationSeconds.value;
|
|
|
|
// 检查是否已接通(通话存在且时长大于0)
|
|
if (callSession != null && duration > 0) {
|
|
_isCallConnected = true;
|
|
_callDuration = Duration(seconds: duration);
|
|
timer.cancel();
|
|
setState(() {});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/// 格式化通话时长
|
|
String _formatDuration(Duration duration) {
|
|
String twoDigits(int n) => n.toString().padLeft(2, '0');
|
|
final minutes = twoDigits(duration.inMinutes.remainder(60));
|
|
final seconds = twoDigits(duration.inSeconds.remainder(60));
|
|
return '$minutes:$seconds';
|
|
}
|
|
|
|
/// 切换麦克风状态
|
|
void _toggleMic() {
|
|
setState(() {
|
|
_isMicMuted = !_isMicMuted;
|
|
});
|
|
// TODO: 调用RTC Manager切换麦克风
|
|
// _rtcManager.enableAudio(!_isMicMuted);
|
|
}
|
|
|
|
/// 切换扬声器状态
|
|
void _toggleSpeaker() {
|
|
setState(() {
|
|
_isSpeakerOn = !_isSpeakerOn;
|
|
});
|
|
// TODO: 调用RTC Manager切换扬声器
|
|
// _rtcManager.setEnableSpeakerphone(_isSpeakerOn);
|
|
}
|
|
|
|
/// 挂断通话
|
|
void _hangUp() async {
|
|
try {
|
|
// TODO: 离开RTC频道
|
|
// await _rtcManager.leaveChannel();
|
|
|
|
// 结束通话(传递通话时长)
|
|
await _callController.endCall(callDuration: _callDuration.inSeconds);
|
|
|
|
// 返回上一页
|
|
Get.back();
|
|
} catch (e) {
|
|
print('挂断通话失败: $e');
|
|
Get.back();
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return PopScope(
|
|
canPop: false, // 禁止手势返回
|
|
onPopInvoked: (didPop) {
|
|
// 已经禁止返回,所以这里不会被调用
|
|
// 如果需要返回,应该通过挂断按钮或其他明确的操作
|
|
},
|
|
child: Scaffold(
|
|
backgroundColor: Colors.black,
|
|
body: Stack(
|
|
children: [
|
|
// 背景视频/头像(模糊)
|
|
_buildBackground(),
|
|
|
|
// 最小化按钮(左上角)
|
|
_buildMinimizeButton(),
|
|
|
|
// 用户信息
|
|
_buildUserInfo(),
|
|
|
|
// 通话时长
|
|
_buildCallDuration(),
|
|
|
|
// 底部控制按钮
|
|
_buildControlButtons(),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 构建最小化按钮
|
|
Widget _buildMinimizeButton() {
|
|
return Positioned(
|
|
top: 26.w,
|
|
left: 26.w,
|
|
child: GestureDetector(
|
|
onTap: _minimizeCall,
|
|
child: Image.asset(Assets.imagesCloseArrow, width: 20.w, height: 20.w),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 最小化通话
|
|
void _minimizeCall() {
|
|
// 显示视频通话小窗
|
|
if (Get.isRegistered<OverlayController>()) {
|
|
final overlayController = Get.find<OverlayController>();
|
|
overlayController.showVideoCall(
|
|
targetUserId: widget.targetUserId,
|
|
targetUserName: _targetUserName,
|
|
targetAvatarUrl: _targetAvatarUrl,
|
|
);
|
|
}
|
|
// 返回上一页
|
|
Get.back();
|
|
}
|
|
|
|
/// 构建背景
|
|
Widget _buildBackground() {
|
|
final callSession = _callController.currentCall.value;
|
|
final isVideoCall = callSession != null && callSession.callType == CallType.video;
|
|
|
|
// 如果是视频通话,显示本地视频视图
|
|
if (isVideoCall && _localVideoViewController != null) {
|
|
Get.log('显示本地视频视图$_localVideoViewController');
|
|
return SizedBox(
|
|
width: double.infinity,
|
|
height: 1.sh,
|
|
child: AgoraVideoView(
|
|
controller: _localVideoViewController!,
|
|
),
|
|
);
|
|
}
|
|
|
|
// 否则显示模糊的头像背景
|
|
return SizedBox(
|
|
width: double.infinity,
|
|
height: 1.sh,
|
|
child: _targetAvatarUrl != null && _targetAvatarUrl!.isNotEmpty
|
|
? ImageFiltered(
|
|
imageFilter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
|
|
child: CachedNetworkImage(
|
|
imageUrl: _targetAvatarUrl!,
|
|
fit: BoxFit.cover,
|
|
width: double.infinity,
|
|
height: double.infinity,
|
|
errorWidget: (context, url, error) => _buildDefaultBackground(),
|
|
),
|
|
)
|
|
: _buildDefaultBackground(),
|
|
);
|
|
}
|
|
|
|
/// 构建默认背景
|
|
Widget _buildDefaultBackground() {
|
|
return Container(
|
|
color: Colors.black,
|
|
);
|
|
}
|
|
|
|
/// 构建用户信息
|
|
Widget _buildUserInfo() {
|
|
return Positioned(
|
|
top: MediaQuery.of(context).size.height * 0.15,
|
|
left: 0,
|
|
right: 0,
|
|
child: Column(
|
|
children: [
|
|
// 头像
|
|
ClipOval(
|
|
child: _targetAvatarUrl != null && _targetAvatarUrl!.isNotEmpty
|
|
? CachedNetworkImage(
|
|
imageUrl: _targetAvatarUrl!,
|
|
width: 120.w,
|
|
height: 120.w,
|
|
fit: BoxFit.cover,
|
|
errorWidget: (context, url, error) => Image.asset(
|
|
Assets.imagesUserAvatar,
|
|
width: 120.w,
|
|
height: 120.w,
|
|
fit: BoxFit.cover,
|
|
),
|
|
)
|
|
: Image.asset(
|
|
Assets.imagesUserAvatar,
|
|
width: 120.w,
|
|
height: 120.w,
|
|
fit: BoxFit.cover,
|
|
),
|
|
),
|
|
SizedBox(height: 16.h),
|
|
// 用户名
|
|
Text(
|
|
_targetUserName ?? widget.targetUserId,
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 24.sp,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 构建通话时长/状态文本
|
|
Widget _buildCallDuration() {
|
|
// 如果是被呼叫方且未接通,显示邀请文字
|
|
if (!widget.isInitiator && !_isCallConnected) {
|
|
final callSession = _callController.currentCall.value;
|
|
final isVideoCall = callSession != null && callSession.callType == CallType.video;
|
|
final inviteText = isVideoCall ? '邀请你视频通话' : '邀请你语音通话';
|
|
|
|
return Positioned(
|
|
bottom: MediaQuery.of(context).size.height * 0.25,
|
|
left: 0,
|
|
right: 0,
|
|
child: Center(
|
|
child: Text(
|
|
inviteText,
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 16.sp,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// 呼叫方或已接通,显示时长或"正在呼叫中"
|
|
return Positioned(
|
|
bottom: MediaQuery.of(context).size.height * 0.25,
|
|
left: 0,
|
|
right: 0,
|
|
child: Center(
|
|
child: Text(
|
|
_isCallConnected ? _formatDuration(_callDuration) : '正在呼叫中',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 16.sp,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 构建控制按钮
|
|
Widget _buildControlButtons() {
|
|
// 如果是被呼叫方且未接通,显示"拒绝"和"接听"按钮
|
|
if (!widget.isInitiator && !_isCallConnected) {
|
|
return Positioned(
|
|
bottom: 40.h,
|
|
left: 0,
|
|
right: 0,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
// 拒绝按钮
|
|
_buildControlButton(
|
|
icon: Icons.call_end,
|
|
label: '拒绝',
|
|
isActive: true,
|
|
onTap: _rejectCall,
|
|
isReject: true,
|
|
),
|
|
// 接听按钮
|
|
_buildControlButton(
|
|
icon: Icons.phone,
|
|
label: '接听',
|
|
isActive: true,
|
|
onTap: _acceptCall,
|
|
isAccept: true,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// 呼叫方或已接通,显示常规控制按钮
|
|
return Positioned(
|
|
bottom: 40.h,
|
|
left: 0,
|
|
right: 0,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
// 免提按钮
|
|
_buildControlButton(
|
|
icon: Icons.volume_up,
|
|
label: '免提',
|
|
isActive: _isSpeakerOn,
|
|
onTap: _toggleSpeaker,
|
|
),
|
|
// 麦克风按钮
|
|
_buildControlButton(
|
|
icon: Icons.mic,
|
|
label: '麦克风',
|
|
isActive: !_isMicMuted,
|
|
onTap: _toggleMic,
|
|
),
|
|
// 挂断按钮
|
|
_buildControlButton(
|
|
icon: Icons.call_end,
|
|
label: '挂断',
|
|
isActive: true,
|
|
onTap: _hangUp,
|
|
isHangUp: true,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 构建控制按钮
|
|
Widget _buildControlButton({
|
|
required IconData icon,
|
|
required String label,
|
|
required bool isActive,
|
|
required VoidCallback onTap,
|
|
bool isHangUp = false,
|
|
bool isReject = false,
|
|
bool isAccept = false,
|
|
}) {
|
|
Color buttonColor;
|
|
if (isHangUp || isReject) {
|
|
buttonColor = Color(0xFFFF3B30); // 红色
|
|
} else if (isAccept) {
|
|
buttonColor = Color(0xFF34C759); // 绿色
|
|
} else {
|
|
buttonColor = isActive
|
|
? Colors.white.withOpacity(0.3)
|
|
: Colors.white.withOpacity(0.2);
|
|
}
|
|
|
|
return GestureDetector(
|
|
onTap: onTap,
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
width: 56.w,
|
|
height: 56.w,
|
|
decoration: BoxDecoration(
|
|
color: buttonColor,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(
|
|
icon,
|
|
color: Colors.white,
|
|
size: 28.w,
|
|
),
|
|
),
|
|
SizedBox(height: 8.h),
|
|
Text(
|
|
label,
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 12.sp,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
/// 接听通话
|
|
Future<void> _acceptCall() async {
|
|
try {
|
|
// 尝试从 ChatController 获取最近的通话消息
|
|
ChatController? chatController;
|
|
EMMessage? callMessage;
|
|
|
|
try {
|
|
final tag = 'chat_${widget.targetUserId}';
|
|
if (Get.isRegistered<ChatController>(tag: tag)) {
|
|
chatController = Get.find<ChatController>(tag: tag);
|
|
// 查找最近的通话邀请消息(从后往前找,找到第一条通话消息)
|
|
final messages = chatController.messages;
|
|
for (var i = messages.length - 1; i >= 0; i--) {
|
|
final msg = messages[i];
|
|
if (msg.body.type == MessageType.CUSTOM) {
|
|
final customBody = msg.body as EMCustomMessageBody;
|
|
// 检查 event 是否为 'call'
|
|
if (customBody.event == 'call') {
|
|
final params = customBody.params;
|
|
// 检查通话状态是否为未接听状态(missed 或 calling)
|
|
if (params != null) {
|
|
final callStatus = params['callStatus'];
|
|
if (callStatus == 'missed' || callStatus == 'calling') {
|
|
callMessage = msg;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
print('⚠️ [VideoCallPage] 获取ChatController失败: $e');
|
|
}
|
|
|
|
if (callMessage == null) {
|
|
SmartDialog.showToast('未找到通话邀请消息');
|
|
return;
|
|
}
|
|
|
|
final accepted = await _callController.acceptCall(
|
|
message: callMessage,
|
|
chatController: chatController,
|
|
);
|
|
|
|
if (accepted) {
|
|
// 通话已接通,UI会自动更新
|
|
print('✅ [VideoCallPage] 通话已接通');
|
|
} else {
|
|
SmartDialog.showToast('接听失败');
|
|
}
|
|
} catch (e) {
|
|
print('❌ [VideoCallPage] 接听通话失败: $e');
|
|
SmartDialog.showToast('接听失败: $e');
|
|
}
|
|
}
|
|
|
|
/// 拒绝通话
|
|
Future<void> _rejectCall() async {
|
|
try {
|
|
// 尝试从 ChatController 获取最近的通话消息
|
|
ChatController? chatController;
|
|
EMMessage? callMessage;
|
|
|
|
try {
|
|
final tag = 'chat_${widget.targetUserId}';
|
|
if (Get.isRegistered<ChatController>(tag: tag)) {
|
|
chatController = Get.find<ChatController>(tag: tag);
|
|
// 查找最近的通话邀请消息(从后往前找,找到第一条通话消息)
|
|
final messages = chatController.messages;
|
|
for (var i = messages.length - 1; i >= 0; i--) {
|
|
final msg = messages[i];
|
|
if (msg.body.type == MessageType.CUSTOM) {
|
|
final customBody = msg.body as EMCustomMessageBody;
|
|
// 检查 event 是否为 'call'
|
|
if (customBody.event == 'call') {
|
|
final params = customBody.params;
|
|
// 检查通话状态是否为未接听状态(missed 或 calling)
|
|
if (params != null) {
|
|
final callStatus = params['callStatus'];
|
|
if (callStatus == 'missed' || callStatus == 'calling') {
|
|
callMessage = msg;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
print('⚠️ [VideoCallPage] 获取ChatController失败: $e');
|
|
}
|
|
|
|
if (callMessage == null) {
|
|
// 即使没有找到消息,也执行拒绝操作(关闭页面)
|
|
await _callController.endCall(callDuration: 0);
|
|
Get.back();
|
|
return;
|
|
}
|
|
|
|
final rejected = await _callController.rejectCall(
|
|
message: callMessage,
|
|
chatController: chatController,
|
|
);
|
|
|
|
if (rejected) {
|
|
// 拒绝成功,返回上一页
|
|
Get.back();
|
|
}
|
|
} catch (e) {
|
|
print('❌ [VideoCallPage] 拒绝通话失败: $e');
|
|
// 即使失败也返回上一页
|
|
Get.back();
|
|
}
|
|
}
|
|
}
|
|
|