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.
 
 
 
 
 

639 lines
18 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 String? callType;
final String? channelId;
final MarriageData? userData;
final bool isInitiator; // 是否是发起方
final EMMessage? callMessage; // 通话消息(用于接听通话时更新消息状态)
const VideoCallPage({
super.key,
required this.targetUserId,
this.callType,
this.channelId,
this.userData,
this.isInitiator = true,
this.callMessage,
});
@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 isVideoCall = widget.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 {
if (widget.callMessage == null) {
SmartDialog.showToast('未找到通话邀请消息');
return;
}
// 尝试获取 ChatController
ChatController? chatController;
try {
final tag = 'chat_${widget.targetUserId}';
if (Get.isRegistered<ChatController>(tag: tag)) {
chatController = Get.find<ChatController>(tag: tag);
}
} catch (e) {
print('⚠️ [VideoCallPage] 获取ChatController失败: $e');
}
final accepted = await _callController.acceptCall(
message: widget.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;
try {
final tag = 'chat_${widget.targetUserId}';
if (Get.isRegistered<ChatController>(tag: tag)) {
chatController = Get.find<ChatController>(tag: tag);
}
} catch (e) {
print('⚠️ [VideoCallPage] 获取ChatController失败: $e');
}
if (widget.callMessage == null) {
// 即使没有消息,也执行拒绝操作(关闭页面)
await _callController.endCall(callDuration: 0);
Get.back();
return;
}
final rejected = await _callController.rejectCall(
message: widget.callMessage!,
chatController: chatController,
);
if (rejected) {
// 拒绝成功,返回上一页
Get.back();
}
} catch (e) {
print('❌ [VideoCallPage] 拒绝通话失败: $e');
// 即使失败也返回上一页
Get.back();
}
}
}