|
|
|
@ -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, |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
); |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
/// 构建控制按钮 |
|
|
|
|