diff --git a/lib/pages/message/video_call_page.dart b/lib/pages/message/video_call_page.dart index eca74ce..b4a7b62 100644 --- a/lib/pages/message/video_call_page.dart +++ b/lib/pages/message/video_call_page.dart @@ -42,16 +42,16 @@ class VideoCallPage extends StatefulWidget { 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; @@ -62,14 +62,12 @@ class _VideoCallPageState extends State { _initCallStatus(); _startDurationTimer(); _initLocalVideo(); - + // 设置系统UI样式 SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); - SystemChrome.setPreferredOrientations([ - DeviceOrientation.portraitUp, - ]); + SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); } - + /// 初始化本地视频视图 void _initLocalVideo() { final callSession = _callController.currentCall.value; @@ -89,7 +87,7 @@ class _VideoCallPageState extends State { void _initCallStatus() { // 不需要初始化,直接使用 CallController 的响应式变量 } - + /// 判断通话是否已接通 bool get _isCallConnected { final callSession = _callController.currentCall.value; @@ -101,8 +99,8 @@ class _VideoCallPageState extends State { // 优先使用传入的 userData if (widget.userData != null) { setState(() { - _targetUserName = widget.userData!.nickName.isNotEmpty - ? widget.userData!.nickName + _targetUserName = widget.userData!.nickName.isNotEmpty + ? widget.userData!.nickName : widget.targetUserId; _targetAvatarUrl = widget.userData!.profilePhoto; }); @@ -112,10 +110,14 @@ class _VideoCallPageState extends State { // 如果没有传入 userData,尝试从 ConversationController 获取 if (Get.isRegistered()) { final conversationController = Get.find(); - + // 先从缓存中获取 - final cachedUserInfo = conversationController.getCachedUserInfo(widget.targetUserId); - if (cachedUserInfo != null && (cachedUserInfo.nickName != null || cachedUserInfo.avatarUrl != null)) { + final cachedUserInfo = conversationController.getCachedUserInfo( + widget.targetUserId, + ); + if (cachedUserInfo != null && + (cachedUserInfo.nickName != null || + cachedUserInfo.avatarUrl != null)) { setState(() { _targetUserName = cachedUserInfo.nickName ?? widget.targetUserId; _targetAvatarUrl = cachedUserInfo.avatarUrl; @@ -124,8 +126,11 @@ class _VideoCallPageState extends State { } // 如果缓存中没有,尝试从 IM 加载 - final userInfo = await conversationController.loadContact(widget.targetUserId); - if (userInfo != null && (userInfo.nickName != null || userInfo.avatarUrl != null)) { + 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; @@ -146,7 +151,10 @@ class _VideoCallPageState extends State { _durationTimer?.cancel(); _hideControlsTimer?.cancel(); _localVideoViewController?.dispose(); - SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); + SystemChrome.setEnabledSystemUIMode( + SystemUiMode.manual, + overlays: SystemUiOverlay.values, + ); SystemChrome.setPreferredOrientations(DeviceOrientation.values); super.dispose(); } @@ -158,7 +166,8 @@ class _VideoCallPageState extends State { if (mounted) { final wasConnected = _isCallConnected; // 如果通话存在且已经开始计时,说明已接通 - if (callSession != null && _callController.callDurationSeconds.value > 0) { + if (callSession != null && + _callController.callDurationSeconds.value > 0) { if (!wasConnected) { // 刚接通,启动5秒隐藏定时器 _startHideControlsTimer(); @@ -169,7 +178,7 @@ class _VideoCallPageState extends State { } } }); - + // 监听通话时长变化(已接通时启动隐藏定时器) _callController.callDurationSeconds.listen((seconds) { if (mounted && !_isCallConnected && seconds > 0) { @@ -193,9 +202,9 @@ class _VideoCallPageState extends State { /// 切换控制按钮的显示/隐藏(点击屏幕时调用) void _toggleControlsVisibility() { if (!_isCallConnected) return; // 未接通时不处理 - + showControls.value = !showControls.value; - + // 如果显示控制按钮,重新启动5秒隐藏定时器 if (showControls.value) { _startHideControlsTimer(); @@ -245,16 +254,19 @@ class _VideoCallPageState extends State { children: [ // 背景视频/头像(模糊) _buildBackground(), - + // 最小化按钮(左上角) _buildMinimizeButton(), - + + // 本地视频小窗口(右上角,视频通话且已接通时显示) + _buildLocalVideoPreview(), + // 用户信息 _buildUserInfo(), - + // 通话时长 _buildCallDuration(), - + // 底部控制按钮 _buildControlButtons(), ], @@ -297,31 +309,38 @@ class _VideoCallPageState extends State { return Obx(() { // 在 Obx 中访问响应式变量,确保建立监听关系 final callSession = _callController.currentCall.value; - final isVideoCall = callSession != null && callSession.callType == CallType.video; + 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'); - + + 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}'); + 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}'); + print( + '📞 [VideoCallPage] currentRemoteUid 不为 null: $currentRemoteUid, engine: ${engine != null}', + ); if (engine != null) { print('📞 [VideoCallPage] 显示远端视频视图,UID:$currentRemoteUid'); final remoteVideoViewController = VideoViewController( @@ -331,10 +350,10 @@ class _VideoCallPageState extends State { return SizedBox( width: double.infinity, height: 1.sh, - key: ValueKey('remote_video_$currentRemoteUid'), // 使用 key 确保 remoteUid 变化时重建 - child: AgoraVideoView( - controller: remoteVideoViewController, - ), + key: ValueKey( + 'remote_video_$currentRemoteUid', + ), // 使用 key 确保 remoteUid 变化时重建 + child: AgoraVideoView(controller: remoteVideoViewController), ); } else { print('⚠️ [VideoCallPage] engine 为 null,无法显示远端视频'); @@ -342,25 +361,76 @@ class _VideoCallPageState extends State { } else { print('⚠️ [VideoCallPage] currentRemoteUid 为 null,无法显示远端视频'); } - + // 如果没有远端视频,显示本地视频视图(自己的画面) if (_localVideoViewController != null) { print('📞 [VideoCallPage] 显示本地视频视图'); return SizedBox( width: double.infinity, height: 1.sh, - child: AgoraVideoView( - controller: _localVideoViewController!, - ), + 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( @@ -383,9 +453,7 @@ class _VideoCallPageState extends State { /// 构建默认背景 Widget _buildDefaultBackground() { - return Container( - color: Colors.black, - ); + return Container(color: Colors.black); } /// 构建用户信息 @@ -395,14 +463,19 @@ class _VideoCallPageState extends State { 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) { + 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, @@ -454,12 +527,12 @@ class _VideoCallPageState extends State { 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, @@ -476,12 +549,12 @@ class _VideoCallPageState extends State { ), ); } - + // 如果已接通但控制按钮已隐藏,不显示时长 if (isCallConnected && !showControls.value) { return const SizedBox.shrink(); } - + // 呼叫方或已接通,显示时长或"正在呼叫中" final duration = Duration(seconds: callDuration); return Positioned( @@ -509,7 +582,7 @@ class _VideoCallPageState extends State { final callSession = _callController.currentCall.value; final callDuration = _callController.callDurationSeconds.value; final isCallConnected = callSession != null && callDuration > 0; - + // 如果是被呼叫方且未接通,显示"拒绝"和"接听"按钮 if (!widget.isInitiator && !isCallConnected) { return Positioned( @@ -539,12 +612,12 @@ class _VideoCallPageState extends State { ), ); } - + // 如果已接通但控制按钮已隐藏,不显示按钮 if (isCallConnected && !showControls.value) { return const SizedBox.shrink(); } - + // 呼叫方或已接通,显示常规控制按钮 return Positioned( bottom: 40.h, @@ -597,11 +670,9 @@ class _VideoCallPageState extends State { } else if (isAccept) { buttonColor = Color(0xFF34C759); // 绿色 } else { - buttonColor = isActive - ? Colors.white.withOpacity(0.3) - : Colors.white.withOpacity(0.2); + buttonColor = isActive ? Colors.white : Colors.white.withOpacity(0.2); } - + return GestureDetector( onTap: onTap, child: Column( @@ -615,23 +686,22 @@ class _VideoCallPageState extends State { ), child: Icon( icon, - color: Colors.white, + 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, - ), + style: TextStyle(color: Colors.white, fontSize: 12.sp), ), ], ), ); } - + /// 接听通话 Future _acceptCall() async { if (widget.callMessage == null) { @@ -645,20 +715,21 @@ class _VideoCallPageState extends State { if (Get.isRegistered(tag: tag)) { chatController = Get.find(tag: tag); } - + final accepted = await _callController.acceptCall( message: widget.callMessage!, chatController: chatController, ); - + if (accepted) { - // 通话已接通,UI会自动更新 - print('✅ [VideoCallPage] 通话已接通'); + // 通话已接通,重新初始化本地视频视图(接收方接听后需要初始化) + _initLocalVideo(); + print('✅ [VideoCallPage] 通话已接通,已重新初始化本地视频视图'); } else { SmartDialog.showToast('接听失败'); } } - + /// 拒绝通话 Future _rejectCall() async { // 尝试获取 ChatController @@ -667,16 +738,15 @@ class _VideoCallPageState extends State { if (Get.isRegistered(tag: tag)) { chatController = Get.find(tag: tag); } - + final rejected = await _callController.rejectCall( message: widget.callMessage!, chatController: chatController, ); - + if (rejected) { // 拒绝成功,返回上一页 Get.back(); } } } -