From 656f9d81cb4d1cedf58a0ca1c5d2d01c776f4cb2 Mon Sep 17 00:00:00 2001 From: Jolie <412895109@qq.com> Date: Tue, 30 Dec 2025 07:31:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(video-call):=20=E5=AE=9E=E7=8E=B0=E9=80=9A?= =?UTF-8?q?=E8=AF=9D=E6=8E=A7=E5=88=B6=E5=8A=9F=E8=83=BD=E5=92=8C=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加麦克风静音和扬声器开关状态管理 - 实现toggleMic和toggleSpeaker方法控制音频设备 - 添加hangUpCall方法处理挂断通话逻辑 - 优化视频通话页面控制按钮显示逻辑 - 实现通话接通后5秒自动隐藏控制按钮功能 - 添加点击屏幕切换控制按钮显示/隐藏功能 - 重构通话状态管理使用响应式变量 - 更新RTCManager添加扬声器控制接口 --- lib/controller/message/call_controller.dart | 56 +++ lib/pages/message/video_call_page.dart | 384 ++++++++++---------- lib/rtc/rtc_manager.dart | 10 + 3 files changed, 259 insertions(+), 191 deletions(-) diff --git a/lib/controller/message/call_controller.dart b/lib/controller/message/call_controller.dart index c41b5c7..65ebe19 100644 --- a/lib/controller/message/call_controller.dart +++ b/lib/controller/message/call_controller.dart @@ -83,6 +83,12 @@ class CallController extends GetxController { // 远端用户UID(用于显示远端视频) final Rxn remoteUid = Rxn(); + // 麦克风静音状态 + final RxBool isMicMuted = false.obs; + + // 扬声器开启状态 + final RxBool isSpeakerOn = false.obs; + // 音频播放器(用于播放来电铃声) final AudioPlayer _callAudioPlayer = AudioPlayer(); bool _isPlayingCallAudio = false; @@ -734,12 +740,62 @@ class CallController extends GetxController { return false; } + /// 切换麦克风状态 + Future toggleMic() async { + try { + isMicMuted.value = !isMicMuted.value; + await RTCManager.instance.muteLocalAudio(isMicMuted.value); + print('📞 [CallController] 麦克风${isMicMuted.value ? "已静音" : "已取消静音"}'); + } catch (e) { + print('❌ [CallController] 切换麦克风状态失败: $e'); + // 如果失败,恢复状态 + isMicMuted.value = !isMicMuted.value; + } + } + + /// 切换扬声器状态 + Future toggleSpeaker() async { + try { + isSpeakerOn.value = !isSpeakerOn.value; + await RTCManager.instance.setEnableSpeakerphone(isSpeakerOn.value); + print('📞 [CallController] 扬声器${isSpeakerOn.value ? "已开启" : "已关闭"}'); + } catch (e) { + print('❌ [CallController] 切换扬声器状态失败: $e'); + // 如果失败,恢复状态 + isSpeakerOn.value = !isSpeakerOn.value; + } + } + + /// 挂断通话 + Future hangUpCall() async { + try { + // 离开RTC频道 + await RTCManager.instance.leaveChannel(); + + // 结束通话(传递通话时长) + await endCall(callDuration: callDurationSeconds.value); + + print('✅ [CallController] 通话已挂断'); + } catch (e) { + print('❌ [CallController] 挂断通话失败: $e'); + // 即使离开频道失败,也结束通话 + try { + await endCall(callDuration: callDurationSeconds.value); + } catch (e2) { + print('❌ [CallController] 结束通话失败: $e2'); + } + } + } + @override void onClose() { stopCallAudio(); _stopCallTimer(); currentCall.value = null; rtcChannel.value = null; + remoteUid.value = null; + isMicMuted.value = false; + isSpeakerOn.value = false; _callAudioPlayer.dispose(); super.onClose(); } diff --git a/lib/pages/message/video_call_page.dart b/lib/pages/message/video_call_page.dart index e1eb0b6..80ec02f 100644 --- a/lib/pages/message/video_call_page.dart +++ b/lib/pages/message/video_call_page.dart @@ -43,16 +43,14 @@ class _VideoCallPageState extends State { 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; + // 是否显示控制按钮和时长(接通后5秒隐藏) + final RxBool showControls = true.obs; + Timer? _hideControlsTimer; // 本地视频视图控制器 VideoViewController? _localVideoViewController; @@ -89,16 +87,13 @@ class _VideoCallPageState extends State { /// 初始化通话状态 void _initCallStatus() { - // 检查当前通话状态 + // 不需要初始化,直接使用 CallController 的响应式变量 + } + + /// 判断通话是否已接通 + bool get _isCallConnected { final callSession = _callController.currentCall.value; - if (callSession != null && _callController.callDurationSeconds.value > 0) { - // 如果通话已存在且已经开始计时,说明已接通 - _isCallConnected = true; - _callDuration = Duration(seconds: _callController.callDurationSeconds.value); - } else { - // 否则是未接通状态 - _isCallConnected = false; - } + return callSession != null && _callController.callDurationSeconds.value > 0; } /// 加载用户信息 @@ -153,6 +148,7 @@ class _VideoCallPageState extends State { @override void dispose() { _durationTimer?.cancel(); + _hideControlsTimer?.cancel(); _localVideoViewController?.dispose(); SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); SystemChrome.setPreferredOrientations(DeviceOrientation.values); @@ -167,49 +163,48 @@ class _VideoCallPageState extends State { final wasConnected = _isCallConnected; // 如果通话存在且已经开始计时,说明已接通 if (callSession != null && _callController.callDurationSeconds.value > 0) { - _isCallConnected = true; if (!wasConnected) { - // 刚接通,同步时长 - _callDuration = Duration(seconds: _callController.callDurationSeconds.value); + // 刚接通,启动5秒隐藏定时器 + _startHideControlsTimer(); } } else if (callSession == null) { - _isCallConnected = false; + _hideControlsTimer?.cancel(); + showControls.value = true; } - setState(() {}); } }); - // 监听通话时长变化(已接通时更新) + // 监听通话时长变化(已接通时启动隐藏定时器) _callController.callDurationSeconds.listen((seconds) { - if (mounted && _isCallConnected) { - setState(() { - _callDuration = Duration(seconds: seconds); - }); - } else if (mounted && !_isCallConnected && seconds > 0) { + if (mounted && !_isCallConnected && seconds > 0) { // 如果时长开始增加,说明刚接通 - setState(() { - _isCallConnected = true; - _callDuration = Duration(seconds: seconds); - }); + _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; // 未接通时不处理 - // 如果未接通,使用本地计时器检查状态变化 - 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(() {}); - } - } - }); + showControls.value = !showControls.value; + + // 如果显示控制按钮,重新启动5秒隐藏定时器 + if (showControls.value) { + _startHideControlsTimer(); + } else { + _hideControlsTimer?.cancel(); } } @@ -223,37 +218,19 @@ class _VideoCallPageState extends State { /// 切换麦克风状态 void _toggleMic() { - setState(() { - _isMicMuted = !_isMicMuted; - }); - // TODO: 调用RTC Manager切换麦克风 - // _rtcManager.enableAudio(!_isMicMuted); + _callController.toggleMic(); } /// 切换扬声器状态 void _toggleSpeaker() { - setState(() { - _isSpeakerOn = !_isSpeakerOn; - }); - // TODO: 调用RTC Manager切换扬声器 - // _rtcManager.setEnableSpeakerphone(_isSpeakerOn); + _callController.toggleSpeaker(); } /// 挂断通话 void _hangUp() async { - try { - // TODO: 离开RTC频道 - // await _rtcManager.leaveChannel(); - - // 结束通话(传递通话时长) - await _callController.endCall(callDuration: _callDuration.inSeconds); - - // 返回上一页 - Get.back(); - } catch (e) { - print('挂断通话失败: $e'); - Get.back(); - } + await _callController.hangUpCall(); + // 返回上一页 + Get.back(); } @override @@ -266,23 +243,26 @@ class _VideoCallPageState extends State { }, child: Scaffold( backgroundColor: Colors.black, - body: Stack( - children: [ - // 背景视频/头像(模糊) - _buildBackground(), - - // 最小化按钮(左上角) - _buildMinimizeButton(), - - // 用户信息 - _buildUserInfo(), - - // 通话时长 - _buildCallDuration(), - - // 底部控制按钮 - _buildControlButtons(), - ], + body: GestureDetector( + onTap: _toggleControlsVisibility, + child: Stack( + children: [ + // 背景视频/头像(模糊) + _buildBackground(), + + // 最小化按钮(左上角) + _buildMinimizeButton(), + + // 用户信息 + _buildUserInfo(), + + // 通话时长 + _buildCallDuration(), + + // 底部控制按钮 + _buildControlButtons(), + ], + ), ), ), ); @@ -412,63 +392,95 @@ class _VideoCallPageState extends State { /// 构建用户信息 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( + return Obx(() { + // 如果已接通,不显示头像和昵称 + 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, ), - ) - : 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, ), - ), - ], - ), - ); + 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 Obx(() { + // 如果是被呼叫方且未接通,显示邀请文字 + 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: _callController.callDurationSeconds.value); return Positioned( bottom: MediaQuery.of(context).size.height * 0.25, left: 0, right: 0, child: Center( child: Text( - inviteText, + _isCallConnected ? _formatDuration(duration) : '正在呼叫中', style: TextStyle( color: Colors.white, fontSize: 16.sp, @@ -477,30 +489,48 @@ class _VideoCallPageState extends State { ), ), ); - } - - // 呼叫方或已接通,显示时长或"正在呼叫中" - 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 Obx(() { + // 如果是被呼叫方且未接通,显示"拒绝"和"接听"按钮 + 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, @@ -508,60 +538,32 @@ class _VideoCallPageState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - // 拒绝按钮 + // 免提按钮 _buildControlButton( - icon: Icons.call_end, - label: '拒绝', - isActive: true, - onTap: _rejectCall, - isReject: true, + 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.phone, - label: '接听', + icon: Icons.call_end, + label: '挂断', isActive: true, - onTap: _acceptCall, - isAccept: true, + onTap: _hangUp, + isHangUp: 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, - ), - ], - ), - ); + }); } /// 构建控制按钮 diff --git a/lib/rtc/rtc_manager.dart b/lib/rtc/rtc_manager.dart index 7739770..1af0895 100644 --- a/lib/rtc/rtc_manager.dart +++ b/lib/rtc/rtc_manager.dart @@ -504,6 +504,16 @@ class RTCManager { print('本地视频${enabled ? "已开启" : "已关闭"}'); } + /// 设置扬声器开关 + /// [enabled] true表示开启扬声器,false表示关闭(使用听筒) + Future setEnableSpeakerphone(bool enabled) async { + if (_engine == null) { + throw Exception('RTC Engine not initialized'); + } + await _engine!.setEnableSpeakerphone(enabled); + print('扬声器${enabled ? "已开启" : "已关闭"}'); + } + /// 设置客户端角色(仅用于直播场景) /// [role] 客户端角色:主播或观众 Future setClientRole({