|
|
|
@ -1,12 +1,17 @@ |
|
|
|
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'; |
|
|
|
@ -30,6 +35,7 @@ class VideoCallPage extends StatefulWidget { |
|
|
|
|
|
|
|
class _VideoCallPageState extends State<VideoCallPage> { |
|
|
|
final CallController _callController = CallController.instance; |
|
|
|
final RTCManager _rtcManager = RTCManager.instance; |
|
|
|
|
|
|
|
bool _isMicMuted = false; |
|
|
|
bool _isSpeakerOn = false; |
|
|
|
@ -41,14 +47,17 @@ class _VideoCallPageState extends State<VideoCallPage> { |
|
|
|
|
|
|
|
// 通话是否已接通 |
|
|
|
bool _isCallConnected = false; |
|
|
|
|
|
|
|
// 本地视频视图控制器 |
|
|
|
VideoViewController? _localVideoViewController; |
|
|
|
|
|
|
|
@override |
|
|
|
void initState() { |
|
|
|
super.initState(); |
|
|
|
_initializeCall(); |
|
|
|
_loadUserInfo(); |
|
|
|
_initCallStatus(); |
|
|
|
_startDurationTimer(); |
|
|
|
_initLocalVideo(); |
|
|
|
|
|
|
|
// 设置系统UI样式 |
|
|
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); |
|
|
|
@ -56,6 +65,21 @@ class _VideoCallPageState extends State<VideoCallPage> { |
|
|
|
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() { |
|
|
|
@ -123,22 +147,12 @@ class _VideoCallPageState extends State<VideoCallPage> { |
|
|
|
@override |
|
|
|
void dispose() { |
|
|
|
_durationTimer?.cancel(); |
|
|
|
_localVideoViewController?.dispose(); |
|
|
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); |
|
|
|
SystemChrome.setPreferredOrientations(DeviceOrientation.values); |
|
|
|
super.dispose(); |
|
|
|
} |
|
|
|
|
|
|
|
/// 初始化通话 |
|
|
|
Future<void> _initializeCall() async { |
|
|
|
try { |
|
|
|
// TODO: 初始化RTC Engine并加入频道 |
|
|
|
// await _rtcManager.initialize(appId: 'your_app_id'); |
|
|
|
// await _rtcManager.joinChannel(token: 'token', channelId: 'channel_id', uid: uid); |
|
|
|
} catch (e) { |
|
|
|
print('初始化通话失败: $e'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 开始通话时长计时器 |
|
|
|
void _startDurationTimer() { |
|
|
|
// 监听 CallController 的通话状态变化 |
|
|
|
@ -297,6 +311,22 @@ class _VideoCallPageState extends State<VideoCallPage> { |
|
|
|
|
|
|
|
/// 构建背景 |
|
|
|
Widget _buildBackground() { |
|
|
|
final callSession = _callController.currentCall.value; |
|
|
|
final isVideoCall = callSession != null && callSession.callType == CallType.video; |
|
|
|
|
|
|
|
// 如果是视频通话,显示本地视频视图 |
|
|
|
if (isVideoCall && _localVideoViewController != null) { |
|
|
|
Get.log('显示本地视频视图$_localVideoViewController'); |
|
|
|
return SizedBox( |
|
|
|
width: double.infinity, |
|
|
|
height: 1.sh, |
|
|
|
child: AgoraVideoView( |
|
|
|
controller: _localVideoViewController!, |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
// 否则显示模糊的头像背景 |
|
|
|
return SizedBox( |
|
|
|
width: double.infinity, |
|
|
|
height: 1.sh, |
|
|
|
@ -367,8 +397,32 @@ class _VideoCallPageState extends State<VideoCallPage> { |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
/// 构建通话时长 |
|
|
|
/// 构建通话时长/状态文本 |
|
|
|
Widget _buildCallDuration() { |
|
|
|
// 如果是被呼叫方且未接通,显示邀请文字 |
|
|
|
if (!widget.isInitiator && !_isCallConnected) { |
|
|
|
final callSession = _callController.currentCall.value; |
|
|
|
final isVideoCall = callSession != null && callSession.callType == 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, |
|
|
|
), |
|
|
|
), |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
// 呼叫方或已接通,显示时长或"正在呼叫中" |
|
|
|
return Positioned( |
|
|
|
bottom: MediaQuery.of(context).size.height * 0.25, |
|
|
|
left: 0, |
|
|
|
@ -388,6 +442,37 @@ class _VideoCallPageState extends State<VideoCallPage> { |
|
|
|
|
|
|
|
/// 构建控制按钮 |
|
|
|
Widget _buildControlButtons() { |
|
|
|
// 如果是被呼叫方且未接通,显示"拒绝"和"接听"按钮 |
|
|
|
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, |
|
|
|
), |
|
|
|
], |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
// 呼叫方或已接通,显示常规控制按钮 |
|
|
|
return Positioned( |
|
|
|
bottom: 40.h, |
|
|
|
left: 0, |
|
|
|
@ -429,7 +514,20 @@ class _VideoCallPageState extends State<VideoCallPage> { |
|
|
|
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.withOpacity(0.3) |
|
|
|
: Colors.white.withOpacity(0.2); |
|
|
|
} |
|
|
|
|
|
|
|
return GestureDetector( |
|
|
|
onTap: onTap, |
|
|
|
child: Column( |
|
|
|
@ -438,9 +536,7 @@ class _VideoCallPageState extends State<VideoCallPage> { |
|
|
|
width: 56.w, |
|
|
|
height: 56.w, |
|
|
|
decoration: BoxDecoration( |
|
|
|
color: isHangUp |
|
|
|
? Color(0xFFFF3B30) |
|
|
|
: (isActive ? Colors.white.withOpacity(0.3) : Colors.white.withOpacity(0.2)), |
|
|
|
color: buttonColor, |
|
|
|
shape: BoxShape.circle, |
|
|
|
), |
|
|
|
child: Icon( |
|
|
|
@ -461,5 +557,122 @@ class _VideoCallPageState extends State<VideoCallPage> { |
|
|
|
), |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
/// 接听通话 |
|
|
|
Future<void> _acceptCall() async { |
|
|
|
try { |
|
|
|
// 尝试从 ChatController 获取最近的通话消息 |
|
|
|
ChatController? chatController; |
|
|
|
EMMessage? callMessage; |
|
|
|
|
|
|
|
try { |
|
|
|
final tag = 'chat_${widget.targetUserId}'; |
|
|
|
if (Get.isRegistered<ChatController>(tag: tag)) { |
|
|
|
chatController = Get.find<ChatController>(tag: tag); |
|
|
|
// 查找最近的通话邀请消息(从后往前找,找到第一条通话消息) |
|
|
|
final messages = chatController.messages; |
|
|
|
for (var i = messages.length - 1; i >= 0; i--) { |
|
|
|
final msg = messages[i]; |
|
|
|
if (msg.body.type == MessageType.CUSTOM) { |
|
|
|
final customBody = msg.body as EMCustomMessageBody; |
|
|
|
// 检查 event 是否为 'call' |
|
|
|
if (customBody.event == 'call') { |
|
|
|
final params = customBody.params; |
|
|
|
// 检查通话状态是否为未接听状态(missed 或 calling) |
|
|
|
if (params != null) { |
|
|
|
final callStatus = params['callStatus']; |
|
|
|
if (callStatus == 'missed' || callStatus == 'calling') { |
|
|
|
callMessage = msg; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
print('⚠️ [VideoCallPage] 获取ChatController失败: $e'); |
|
|
|
} |
|
|
|
|
|
|
|
if (callMessage == null) { |
|
|
|
SmartDialog.showToast('未找到通话邀请消息'); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
final accepted = await _callController.acceptCall( |
|
|
|
message: callMessage, |
|
|
|
chatController: chatController, |
|
|
|
); |
|
|
|
|
|
|
|
if (accepted) { |
|
|
|
// 通话已接通,UI会自动更新 |
|
|
|
print('✅ [VideoCallPage] 通话已接通'); |
|
|
|
} else { |
|
|
|
SmartDialog.showToast('接听失败'); |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
print('❌ [VideoCallPage] 接听通话失败: $e'); |
|
|
|
SmartDialog.showToast('接听失败: $e'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// 拒绝通话 |
|
|
|
Future<void> _rejectCall() async { |
|
|
|
try { |
|
|
|
// 尝试从 ChatController 获取最近的通话消息 |
|
|
|
ChatController? chatController; |
|
|
|
EMMessage? callMessage; |
|
|
|
|
|
|
|
try { |
|
|
|
final tag = 'chat_${widget.targetUserId}'; |
|
|
|
if (Get.isRegistered<ChatController>(tag: tag)) { |
|
|
|
chatController = Get.find<ChatController>(tag: tag); |
|
|
|
// 查找最近的通话邀请消息(从后往前找,找到第一条通话消息) |
|
|
|
final messages = chatController.messages; |
|
|
|
for (var i = messages.length - 1; i >= 0; i--) { |
|
|
|
final msg = messages[i]; |
|
|
|
if (msg.body.type == MessageType.CUSTOM) { |
|
|
|
final customBody = msg.body as EMCustomMessageBody; |
|
|
|
// 检查 event 是否为 'call' |
|
|
|
if (customBody.event == 'call') { |
|
|
|
final params = customBody.params; |
|
|
|
// 检查通话状态是否为未接听状态(missed 或 calling) |
|
|
|
if (params != null) { |
|
|
|
final callStatus = params['callStatus']; |
|
|
|
if (callStatus == 'missed' || callStatus == 'calling') { |
|
|
|
callMessage = msg; |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
print('⚠️ [VideoCallPage] 获取ChatController失败: $e'); |
|
|
|
} |
|
|
|
|
|
|
|
if (callMessage == null) { |
|
|
|
// 即使没有找到消息,也执行拒绝操作(关闭页面) |
|
|
|
await _callController.endCall(callDuration: 0); |
|
|
|
Get.back(); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
final rejected = await _callController.rejectCall( |
|
|
|
message: callMessage, |
|
|
|
chatController: chatController, |
|
|
|
); |
|
|
|
|
|
|
|
if (rejected) { |
|
|
|
// 拒绝成功,返回上一页 |
|
|
|
Get.back(); |
|
|
|
} |
|
|
|
} catch (e) { |
|
|
|
print('❌ [VideoCallPage] 拒绝通话失败: $e'); |
|
|
|
// 即使失败也返回上一页 |
|
|
|
Get.back(); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|