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.
 
 
 
 
 

682 lines
21 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;
Timer? _durationTimer;
String? _targetUserName;
String? _targetAvatarUrl;
// 是否显示控制按钮和时长(接通后5秒隐藏)
final RxBool showControls = true.obs;
Timer? _hideControlsTimer;
// 本地视频视图控制器
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() {
// 不需要初始化,直接使用 CallController 的响应式变量
}
/// 判断通话是否已接通
bool get _isCallConnected {
final callSession = _callController.currentCall.value;
return callSession != null && _callController.callDurationSeconds.value > 0;
}
/// 加载用户信息
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 获取
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;
}
}
// 如果都获取不到,使用默认值
setState(() {
_targetUserName = widget.targetUserId;
_targetAvatarUrl = null;
});
}
@override
void dispose() {
_durationTimer?.cancel();
_hideControlsTimer?.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) {
if (!wasConnected) {
// 刚接通,启动5秒隐藏定时器
_startHideControlsTimer();
}
} else if (callSession == null) {
_hideControlsTimer?.cancel();
showControls.value = true;
}
}
});
// 监听通话时长变化(已接通时启动隐藏定时器)
_callController.callDurationSeconds.listen((seconds) {
if (mounted && !_isCallConnected && seconds > 0) {
// 如果时长开始增加,说明刚接通
_startHideControlsTimer();
}
});
}
/// 启动隐藏控制按钮的定时器(5秒后隐藏)
void _startHideControlsTimer() {
_hideControlsTimer?.cancel();
showControls.value = true;
_hideControlsTimer = Timer(Duration(seconds: 5), () {
if (mounted && _isCallConnected) {
showControls.value = false;
}
});
}
/// 切换控制按钮的显示/隐藏(点击屏幕时调用)
void _toggleControlsVisibility() {
if (!_isCallConnected) return; // 未接通时不处理
showControls.value = !showControls.value;
// 如果显示控制按钮,重新启动5秒隐藏定时器
if (showControls.value) {
_startHideControlsTimer();
} else {
_hideControlsTimer?.cancel();
}
}
/// 格式化通话时长
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() {
_callController.toggleMic();
}
/// 切换扬声器状态
void _toggleSpeaker() {
_callController.toggleSpeaker();
}
/// 挂断通话
void _hangUp() async {
await _callController.hangUpCall();
// 返回上一页
Get.back();
}
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false, // 禁止手势返回
onPopInvoked: (didPop) {
// 已经禁止返回,所以这里不会被调用
// 如果需要返回,应该通过挂断按钮或其他明确的操作
},
child: Scaffold(
backgroundColor: Colors.black,
body: GestureDetector(
onTap: _toggleControlsVisibility,
child: 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() {
// 使用 Obx 监听通话状态和远端用户 UID 的变化
return Obx(() {
// 在 Obx 中访问响应式变量,确保建立监听关系
final callSession = _callController.currentCall.value;
final isVideoCall = callSession != null && callSession.callType == CallType.video;
final remoteUid = _callController.remoteUid.value;
final remoteUsers = _rtcManager.remoteUsersNotifier.value;
print('📞 [VideoCallPage] _buildBackground Obx 重建,isVideoCall: $isVideoCall, remoteUid: $remoteUid, remoteUsers: $remoteUsers');
// 如果不是视频通话,显示模糊的头像背景
if (!isVideoCall) {
return _buildAvatarBackground();
}
// 如果 remoteUid 为空,尝试从 RTCManager 的远端用户列表中获取
if (remoteUid == null && remoteUsers.isNotEmpty) {
_callController.remoteUid.value = remoteUsers.first;
print('📞 [VideoCallPage] 从 RTCManager.remoteUsersNotifier 获取到 remoteUid: ${remoteUsers.first}');
// Obx 会自动重建,所以这里不需要手动返回
}
// 再次获取 remoteUid(可能刚刚被设置)
final currentRemoteUid = _callController.remoteUid.value;
// 如果远端用户已加入,显示远端视频视图(对方画面)
if (currentRemoteUid != null) {
final engine = _rtcManager.engine;
print('📞 [VideoCallPage] currentRemoteUid 不为 null: $currentRemoteUid, engine: ${engine != null}');
if (engine != null) {
print('📞 [VideoCallPage] 显示远端视频视图,UID:$currentRemoteUid');
final remoteVideoViewController = VideoViewController(
rtcEngine: engine,
canvas: VideoCanvas(uid: currentRemoteUid),
);
return SizedBox(
width: double.infinity,
height: 1.sh,
key: ValueKey('remote_video_$currentRemoteUid'), // 使用 key 确保 remoteUid 变化时重建
child: AgoraVideoView(
controller: remoteVideoViewController,
),
);
} else {
print('⚠️ [VideoCallPage] engine 为 null,无法显示远端视频');
}
} else {
print('⚠️ [VideoCallPage] currentRemoteUid 为 null,无法显示远端视频');
}
// 如果没有远端视频,显示本地视频视图(自己的画面)
if (_localVideoViewController != null) {
print('📞 [VideoCallPage] 显示本地视频视图');
return SizedBox(
width: double.infinity,
height: 1.sh,
child: AgoraVideoView(
controller: _localVideoViewController!,
),
);
}
// 如果本地视频也没有,显示模糊的头像背景
print('📞 [VideoCallPage] 显示头像背景');
return _buildAvatarBackground();
});
}
/// 构建头像背景
Widget _buildAvatarBackground() {
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 Obx(() {
// 在 Obx 中直接访问响应式变量,确保建立监听关系
final callSession = _callController.currentCall.value;
final callDuration = _callController.callDurationSeconds.value;
final isCallConnected = callSession != null && callDuration > 0;
print('📞 [VideoCallPage] _buildUserInfo Obx 重建,isCallConnected: $isCallConnected, callDuration: $callDuration');
// 如果已接通,不显示头像和昵称(适用于发起方和接收方)
if (isCallConnected) {
return const SizedBox.shrink();
}
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() {
return Obx(() {
// 在 Obx 中直接访问响应式变量,确保建立监听关系
final callSession = _callController.currentCall.value;
final callDuration = _callController.callDurationSeconds.value;
final isCallConnected = callSession != null && callDuration > 0;
// 如果是被呼叫方且未接通,显示邀请文字
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,
),
),
),
);
}
// 如果已接通但控制按钮已隐藏,不显示时长
if (isCallConnected && !showControls.value) {
return const SizedBox.shrink();
}
// 呼叫方或已接通,显示时长或"正在呼叫中"
final duration = Duration(seconds: callDuration);
return Positioned(
bottom: MediaQuery.of(context).size.height * 0.25,
left: 0,
right: 0,
child: Center(
child: Text(
isCallConnected ? _formatDuration(duration) : '正在呼叫中',
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
fontWeight: FontWeight.w500,
),
),
),
);
});
}
/// 构建控制按钮
Widget _buildControlButtons() {
return Obx(() {
// 在 Obx 中直接访问响应式变量,确保建立监听关系
final callSession = _callController.currentCall.value;
final callDuration = _callController.callDurationSeconds.value;
final isCallConnected = callSession != null && callDuration > 0;
// 如果是被呼叫方且未接通,显示"拒绝"和"接听"按钮
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,
),
],
),
);
}
// 如果已接通但控制按钮已隐藏,不显示按钮
if (isCallConnected && !showControls.value) {
return const SizedBox.shrink();
}
// 呼叫方或已接通,显示常规控制按钮
return Positioned(
bottom: 40.h,
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// 免提按钮
_buildControlButton(
icon: Icons.volume_up,
label: '免提',
isActive: _callController.isSpeakerOn.value,
onTap: _toggleSpeaker,
),
// 麦克风按钮
_buildControlButton(
icon: Icons.mic,
label: '麦克风',
isActive: !_callController.isMicMuted.value,
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 {
if (widget.callMessage == null) {
SmartDialog.showToast('未找到通话邀请消息');
return;
}
// 尝试获取 ChatController
ChatController? chatController;
final tag = 'chat_${widget.targetUserId}';
if (Get.isRegistered<ChatController>(tag: tag)) {
chatController = Get.find<ChatController>(tag: tag);
}
final accepted = await _callController.acceptCall(
message: widget.callMessage!,
chatController: chatController,
);
if (accepted) {
// 通话已接通,UI会自动更新
print('✅ [VideoCallPage] 通话已接通');
} else {
SmartDialog.showToast('接听失败');
}
}
/// 拒绝通话
Future<void> _rejectCall() async {
// 尝试获取 ChatController
ChatController? chatController;
final tag = 'chat_${widget.targetUserId}';
if (Get.isRegistered<ChatController>(tag: tag)) {
chatController = Get.find<ChatController>(tag: tag);
}
final rejected = await _callController.rejectCall(
message: widget.callMessage!,
chatController: chatController,
);
if (rejected) {
// 拒绝成功,返回上一页
Get.back();
}
}
}