Browse Source

feat(video-call): 实现通话控制功能和界面优化

- 添加麦克风静音和扬声器开关状态管理
- 实现toggleMic和toggleSpeaker方法控制音频设备
- 添加hangUpCall方法处理挂断通话逻辑
- 优化视频通话页面控制按钮显示逻辑
- 实现通话接通后5秒自动隐藏控制按钮功能
- 添加点击屏幕切换控制按钮显示/隐藏功能
- 重构通话状态管理使用响应式变量
- 更新RTCManager添加扬声器控制接口
master
Jolie 3 months ago
parent
commit
656f9d81cb
3 changed files with 259 additions and 191 deletions
  1. 56
      lib/controller/message/call_controller.dart
  2. 384
      lib/pages/message/video_call_page.dart
  3. 10
      lib/rtc/rtc_manager.dart

56
lib/controller/message/call_controller.dart

@ -83,6 +83,12 @@ class CallController extends GetxController {
// UID
final Rxn<int> remoteUid = Rxn<int>();
//
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<void> 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<void> 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<void> 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();
}

384
lib/pages/message/video_call_page.dart

@ -43,16 +43,14 @@ 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;
// 5
final RxBool showControls = true.obs;
Timer? _hideControlsTimer;
//
VideoViewController? _localVideoViewController;
@ -89,16 +87,13 @@ class _VideoCallPageState extends State<VideoCallPage> {
///
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<VideoCallPage> {
@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<VideoCallPage> {
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<VideoCallPage> {
///
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<VideoCallPage> {
},
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<VideoCallPage> {
///
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<VideoCallPage> {
),
),
);
}
// "正在呼叫中"
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<VideoCallPage> {
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,
),
],
),
);
});
}
///

10
lib/rtc/rtc_manager.dart

@ -504,6 +504,16 @@ class RTCManager {
print('本地视频${enabled ? "已开启" : "已关闭"}');
}
///
/// [enabled] true表示开启扬声器false表示关闭使
Future<void> setEnableSpeakerphone(bool enabled) async {
if (_engine == null) {
throw Exception('RTC Engine not initialized');
}
await _engine!.setEnableSpeakerphone(enabled);
print('扬声器${enabled ? "已开启" : "已关闭"}');
}
///
/// [role]
Future<void> setClientRole({

Loading…
Cancel
Save