Browse Source

feat(call): 实现通话功能集成声网SDK和通话控制

- 集成 agora_rtc_engine 依赖并实现 RTCManager 管理音视频通话
- 添加 RTCType 枚举区分通话和直播类型,实现类型化频道管理
- 在 CallController 中实现语音/视频通话的摄像头状态控制逻辑
- 实现通话中加入 RTC 频道的真实通话功能,支持语音和视频通话
- 在 VideoCallPage 中添加本地视频视图显示和通话状态控制界面
- 实现通话页面的接听/拒绝按钮和通话邀请状态显示功能
- 添加通话消息查找和处理机制,支持通话邀请的接收和响应
master
Jolie 3 months ago
parent
commit
476ef2f848
3 changed files with 320 additions and 52 deletions
  1. 37
      lib/controller/message/call_controller.dart
  2. 245
      lib/pages/message/video_call_page.dart
  3. 90
      lib/rtc/rtc_manager.dart

37
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');

245
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<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();
}
}
}

90
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<RoomController>() &&
channelId != null &&
channelId.isNotEmpty) {
final roomController = Get.find<RoomController>();
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<RoomController>() &&
channelId != null &&
channelId.isNotEmpty) {
final roomController = Get.find<RoomController>();
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<RoomController>() &&
channelId != null &&
channelId.isNotEmpty) {
final roomController = Get.find<RoomController>();
await roomController.fetchRtcChannelDetail(channelId);
// RTC类型判断是否执行 RoomController
// RoomController
if (type == RTCType.live) {
// RoomController fetchRtcChannelDetail
final channelId = connection.channelId;
if (Get.isRegistered<RoomController>() &&
channelId != null &&
channelId.isNotEmpty) {
final roomController = Get.find<RoomController>();
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<RoomController>() &&
channelId != null &&
channelId.isNotEmpty) {
final roomController = Get.find<RoomController>();
await roomController.fetchRtcChannelDetail(channelId);
// RTC类型判断是否执行 RoomController
// RoomController
if (type == RTCType.live) {
// RoomController fetchRtcChannelDetail
final channelId = connection.channelId;
if (Get.isRegistered<RoomController>() &&
channelId != null &&
channelId.isNotEmpty) {
final roomController = Get.find<RoomController>();
await roomController.fetchRtcChannelDetail(channelId);
}
}
if (onUserOffline != null) {
@ -390,12 +408,14 @@ class RTCManager {
/// [token]
/// [channelId] ID
/// [uid] ID0
/// [options]
/// [role]
/// [rtcType] RTC类型
Future<void> 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');
}
///

Loading…
Cancel
Save