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 createState() => _VideoCallPageState(); } class _VideoCallPageState extends State { 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 _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()) { final conversationController = Get.find(); // 先从缓存中获取 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(); // hangUpCall() 方法内部已经调用了 Get.back() 来退出 VideoCallPage // 所以这里不需要再次调用 Get.back(),避免退出两次导致回到MessagePage } @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(), // 本地视频小窗口(右上角,视频通话且已接通时显示) _buildLocalVideoPreview(), // 用户信息 _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()) { final overlayController = Get.find(); 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 _buildLocalVideoPreview() { return Obx(() { // 在 Obx 中访问响应式变量,确保建立监听关系 final callSession = _callController.currentCall.value; final isVideoCall = callSession != null && callSession.callType == CallType.video; final callDuration = _callController.callDurationSeconds.value; final isCallConnected = callSession != null && callDuration > 0; // 如果本地视频视图控制器未初始化,尝试初始化(接收方接听后可能需要) if (isVideoCall && _localVideoViewController == null) { final engine = _rtcManager.engine; if (engine != null) { _localVideoViewController = VideoViewController( rtcEngine: engine, canvas: const VideoCanvas(uid: 0), ); print('📞 [VideoCallPage] 在 _buildLocalVideoPreview 中初始化本地视频视图'); } } // 只在视频通话且已接通时显示本地视频小窗口 if (!isVideoCall || !isCallConnected || _localVideoViewController == null) { return const SizedBox.shrink(); } return Positioned( top: 26.w, right: 26.w, child: Container( width: 120.w, height: 160.w, // 4:3 比例 decoration: BoxDecoration( borderRadius: BorderRadius.circular(8.w), border: Border.all(color: Colors.white.withOpacity(0.3), width: 1), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.3), blurRadius: 8, offset: const Offset(0, 2), ), ], ), clipBehavior: Clip.antiAlias, child: AgoraVideoView(controller: _localVideoViewController!), ), ); }); } /// 构建头像背景 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; final isVideoCall = callSession != null && callSession.callType == CallType.video; print( '📞 [VideoCallPage] _buildUserInfo Obx 重建,isCallConnected: $isCallConnected, callDuration: $callDuration, isVideoCall: $isVideoCall', ); // 如果是视频通话且已接通,不显示头像和昵称(适用于发起方和接收方) // 语音通话即使接通也要显示头像和昵称 if (isCallConnected && isVideoCall) { 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: Column( children: [ Center( child: Text( isCallConnected ? _formatDuration(duration) : '正在呼叫中', style: TextStyle( color: Colors.white, fontSize: 16.sp, fontWeight: FontWeight.w500, ), ), ), Obx(() { final callSession = _callController.currentCall.value; final consumeData = _callController.consumeResponse.value; final callDuration = _callController.callDurationSeconds.value; // 判断是否已接通:通过通话时长来判断,如果时长大于0说明已接通 final isCallConnected = callSession != null && callDuration > 0; // 呼叫中(未接通)或免费时不显示 final isFree = consumeData?.isFree == true; // 只有在已接通且不是免费通话时才显示余额 if (!isCallConnected || isFree) { return const SizedBox.shrink(); } final availableBalance = consumeData?.availableBalance ?? 0; final unitSellingBalance = consumeData?.unitSellingBalance ?? 0; if (availableBalance <= 0 && unitSellingBalance <= 0) { return const SizedBox.shrink(); } return Center( child: Text( '玫瑰剩余$availableBalance支($unitSellingBalance玫瑰/分钟)', 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 : 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: isActive && icon != Icons.call_end && icon != Icons.phone ? Colors.black : Colors.white, size: 28.w, ), ), SizedBox(height: 8.h), Text( label, style: TextStyle(color: Colors.white, fontSize: 12.sp), ), ], ), ); } /// 接听通话 Future _acceptCall() async { if (widget.callMessage == null) { SmartDialog.showToast('未找到通话邀请消息'); return; } // 尝试获取 ChatController ChatController? chatController; final tag = 'chat_${widget.targetUserId}'; if (Get.isRegistered(tag: tag)) { chatController = Get.find(tag: tag); } final accepted = await _callController.acceptCall( message: widget.callMessage!, chatController: chatController, ); if (accepted) { // 通话已接通,重新初始化本地视频视图(接收方接听后需要初始化) _initLocalVideo(); print('✅ [VideoCallPage] 通话已接通,已重新初始化本地视频视图'); } else { SmartDialog.showToast('接听失败'); } } /// 拒绝通话 Future _rejectCall() async { // 尝试获取 ChatController ChatController? chatController; final tag = 'chat_${widget.targetUserId}'; if (Get.isRegistered(tag: tag)) { chatController = Get.find(tag: tag); } final rejected = await _callController.rejectCall( message: widget.callMessage!, chatController: chatController, ); if (rejected) { // 拒绝成功,返回上一页 Get.back(); } } }