From 476ef2f8481b83cce76717d0d49de8bdda1afa43 Mon Sep 17 00:00:00 2001 From: Jolie <412895109@qq.com> Date: Mon, 29 Dec 2025 23:38:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(call):=20=E5=AE=9E=E7=8E=B0=E9=80=9A?= =?UTF-8?q?=E8=AF=9D=E5=8A=9F=E8=83=BD=E9=9B=86=E6=88=90=E5=A3=B0=E7=BD=91?= =?UTF-8?q?SDK=E5=92=8C=E9=80=9A=E8=AF=9D=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 集成 agora_rtc_engine 依赖并实现 RTCManager 管理音视频通话 - 添加 RTCType 枚举区分通话和直播类型,实现类型化频道管理 - 在 CallController 中实现语音/视频通话的摄像头状态控制逻辑 - 实现通话中加入 RTC 频道的真实通话功能,支持语音和视频通话 - 在 VideoCallPage 中添加本地视频视图显示和通话状态控制界面 - 实现通话页面的接听/拒绝按钮和通话邀请状态显示功能 - 添加通话消息查找和处理机制,支持通话邀请的接收和响应 --- lib/controller/message/call_controller.dart | 37 ++- lib/pages/message/video_call_page.dart | 245 ++++++++++++++++++-- lib/rtc/rtc_manager.dart | 90 ++++--- 3 files changed, 320 insertions(+), 52 deletions(-) diff --git a/lib/controller/message/call_controller.dart b/lib/controller/message/call_controller.dart index 1c3bedf..7f11024 100644 --- a/lib/controller/message/call_controller.dart +++ b/lib/controller/message/call_controller.dart @@ -1,9 +1,11 @@ import 'dart:async'; +import 'package:agora_rtc_engine/agora_rtc_engine.dart'; import 'package:audioplayers/audioplayers.dart'; import 'package:dating_touchme_app/generated/assets.dart'; import 'package:dating_touchme_app/im/im_manager.dart'; import 'package:dating_touchme_app/model/rtc/rtc_channel_data.dart'; import 'package:dating_touchme_app/network/network_service.dart'; +import 'package:dating_touchme_app/rtc/rtc_manager.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:im_flutter_sdk/im_flutter_sdk.dart'; @@ -203,8 +205,39 @@ class CallController extends GetxController { startCallAudio(); - // TODO: 这里可以集成实际的通话SDK,发起真正的通话 - // 例如:await RTCManager.instance.startCall(targetUserId, callType); + // 根据通话类型设置摄像头状态 + try { + if (callType == CallType.voice) { + // 语音通话:禁用视频(关闭摄像头) + await RTCManager.instance.disableVideo(); + print('📞 [CallController] 语音通话,已关闭摄像头'); + } else { + // 视频通话:启用视频(打开摄像头) + await RTCManager.instance.enableVideo(); + print('📞 [CallController] 视频通话,已打开摄像头'); + } + } catch (e) { + print('⚠️ [CallController] 设置视频状态失败: $e'); + // 继续执行,不阻止加入频道 + } + + // 加入 RTC 频道,发起真正的通话 + try { + await RTCManager.instance.joinChannel( + token: channelData.token, + channelId: channelData.channelId, + uid: channelData.uid, + role: ClientRoleType.clientRoleBroadcaster, + rtcType: RTCType.call, + ); + print('✅ [CallController] 已加入 RTC 频道: ${channelData.channelId}'); + } catch (e) { + print('❌ [CallController] 加入 RTC 频道失败: $e'); + SmartDialog.showToast('加入通话频道失败'); + currentCall.value = null; + return false; + } + return true; } catch (e) { print('❌ [CallController] 发起通话失败: $e'); diff --git a/lib/pages/message/video_call_page.dart b/lib/pages/message/video_call_page.dart index d0d008d..021a72e 100644 --- a/lib/pages/message/video_call_page.dart +++ b/lib/pages/message/video_call_page.dart @@ -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 { final CallController _callController = CallController.instance; + final RTCManager _rtcManager = RTCManager.instance; bool _isMicMuted = false; bool _isSpeakerOn = false; @@ -41,14 +47,17 @@ class _VideoCallPageState extends State { // 通话是否已接通 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 { 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 { @override void dispose() { _durationTimer?.cancel(); + _localVideoViewController?.dispose(); SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); SystemChrome.setPreferredOrientations(DeviceOrientation.values); super.dispose(); } - /// 初始化通话 - Future _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 { /// 构建背景 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 { ); } - /// 构建通话时长 + /// 构建通话时长/状态文本 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 { /// 构建控制按钮 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 { 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 { 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 { ), ); } + + /// 接听通话 + Future _acceptCall() async { + try { + // 尝试从 ChatController 获取最近的通话消息 + ChatController? chatController; + EMMessage? callMessage; + + try { + final tag = 'chat_${widget.targetUserId}'; + if (Get.isRegistered(tag: tag)) { + chatController = Get.find(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 _rejectCall() async { + try { + // 尝试从 ChatController 获取最近的通话消息 + ChatController? chatController; + EMMessage? callMessage; + + try { + final tag = 'chat_${widget.targetUserId}'; + if (Get.isRegistered(tag: tag)) { + chatController = Get.find(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(); + } + } } diff --git a/lib/rtc/rtc_manager.dart b/lib/rtc/rtc_manager.dart index 5b8d452..10482de 100644 --- a/lib/rtc/rtc_manager.dart +++ b/lib/rtc/rtc_manager.dart @@ -9,6 +9,11 @@ import 'package:get/get.dart'; import '../controller/discover/room_controller.dart'; import '../pages/discover/live_room_page.dart'; +enum RTCType { + call, // 通话(语音/视频通话) + live, // 直播 +} + /// RTC 管理器,负责管理声网音视频通话功能 class RTCManager { /// 频道加入状态通知,用于UI监听 @@ -27,6 +32,7 @@ class RTCManager { static RTCManager get instance => _instance; RtcEngine? _engine; + RTCType type = RTCType.live; bool _isInitialized = false; bool _isInChannel = false; String? _currentChannelId; @@ -121,24 +127,28 @@ class RTCManager { remoteUsersNotifier.value = const []; channelJoinedNotifier.value = true; _currentChannelId = connection.channelId; - print('加入频道成功,频道名:${connection.channelId},耗时:${elapsed}ms'); - - // 调用 RoomController 的 fetchRtcChannelDetail 方法 - final channelId = connection.channelId; - if (Get.isRegistered() && - channelId != null && - channelId.isNotEmpty) { - final roomController = Get.find(); - await roomController.fetchRtcChannelDetail(channelId); - } - - if (connection.localUid == _currentUid) { - await RTMManager.instance.subscribe(_currentChannelId ?? ''); - await RTMManager.instance.publishChannelMessage( - channelName: _currentChannelId ?? '', - message: json.encode({'type': 'join_room', 'uid': _currentUid}), - ); - Get.to(() => const LiveRoomPage(id: 0)); + print('加入频道成功,频道名:${connection.channelId},耗时:${elapsed}ms,类型:$type'); + + // 根据RTC类型判断是否执行 RoomController 的逻辑 + // 只有直播类型才执行 RoomController 的逻辑 + if (type == RTCType.live) { + // 调用 RoomController 的 fetchRtcChannelDetail 方法 + final channelId = connection.channelId; + if (Get.isRegistered() && + channelId != null && + channelId.isNotEmpty) { + final roomController = Get.find(); + await roomController.fetchRtcChannelDetail(channelId); + } + + if (connection.localUid == _currentUid) { + await RTMManager.instance.subscribe(_currentChannelId ?? ''); + await RTMManager.instance.publishChannelMessage( + channelName: _currentChannelId ?? '', + message: json.encode({'type': 'join_room', 'uid': _currentUid}), + ); + Get.to(() => const LiveRoomPage(id: 0)); + } } if (onJoinChannelSuccess != null) { onJoinChannelSuccess!(connection, elapsed); @@ -149,13 +159,17 @@ class RTCManager { print('用户加入,UID:$remoteUid'); _handleRemoteUserJoined(remoteUid); - // 调用 RoomController 的 fetchRtcChannelDetail 方法 - final channelId = connection.channelId; - if (Get.isRegistered() && - channelId != null && - channelId.isNotEmpty) { - final roomController = Get.find(); - await roomController.fetchRtcChannelDetail(channelId); + // 根据RTC类型判断是否执行 RoomController 的逻辑 + // 只有直播类型才执行 RoomController 的逻辑 + if (type == RTCType.live) { + // 调用 RoomController 的 fetchRtcChannelDetail 方法 + final channelId = connection.channelId; + if (Get.isRegistered() && + channelId != null && + channelId.isNotEmpty) { + final roomController = Get.find(); + await roomController.fetchRtcChannelDetail(channelId); + } } if (onUserJoined != null) { @@ -171,13 +185,17 @@ class RTCManager { print('用户离开,UID:$remoteUid,原因:$reason'); _handleRemoteUserOffline(remoteUid); - // 调用 RoomController 的 fetchRtcChannelDetail 方法 - final channelId = connection.channelId; - if (Get.isRegistered() && - channelId != null && - channelId.isNotEmpty) { - final roomController = Get.find(); - await roomController.fetchRtcChannelDetail(channelId); + // 根据RTC类型判断是否执行 RoomController 的逻辑 + // 只有直播类型才执行 RoomController 的逻辑 + if (type == RTCType.live) { + // 调用 RoomController 的 fetchRtcChannelDetail 方法 + final channelId = connection.channelId; + if (Get.isRegistered() && + channelId != null && + channelId.isNotEmpty) { + final roomController = Get.find(); + await roomController.fetchRtcChannelDetail(channelId); + } } if (onUserOffline != null) { @@ -390,12 +408,14 @@ class RTCManager { /// [token] 频道令牌(可选,如果频道未开启鉴权则可以为空字符串) /// [channelId] 频道ID /// [uid] 用户ID(0表示自动分配) - /// [options] 频道媒体选项 + /// [role] 客户端角色 + /// [rtcType] RTC类型,用于区分通话还是直播 Future joinChannel({ String? token, required String channelId, int uid = 0, ClientRoleType role = ClientRoleType.clientRoleBroadcaster, + RTCType rtcType = RTCType.live, }) async { if (_engine == null) { throw Exception('RTC Engine not initialized'); @@ -404,6 +424,8 @@ class RTCManager { print('已经在频道中,先离开当前频道'); await leaveChannel(); } + // 设置RTC类型 + type = rtcType; await setClientRole(role: role); _currentUid = uid; if (role == ClientRoleType.clientRoleBroadcaster) { @@ -422,7 +444,7 @@ class RTCManager { publishMicrophoneTrack: true, ), ); - print('正在加入频道:$channelId,UID:$uid'); + print('正在加入频道:$channelId,UID:$uid,类型:$rtcType'); } /// 离开频道