Browse Source
Merge branch 'master' of http://git.qniao.cn/dating-agency/dating_touchme_app
Merge branch 'master' of http://git.qniao.cn/dating-agency/dating_touchme_app
# Conflicts: # lib/controller/message/chat_controller.dartios
23 changed files with 1362 additions and 67 deletions
Unified View
Diff Options
-
2.gitignore
-
9android/app/build.gradle.kts
-
33lib/controller/global.dart
-
41lib/controller/message/chat_controller.dart
-
6lib/controller/message/voice_player_manager.dart
-
131lib/controller/mine/auth_controller.dart
-
3lib/controller/mine/mine_controller.dart
-
14lib/controller/mine/user_controller.dart
-
87lib/im/im_manager.dart
-
2lib/main.dart
-
31lib/model/mine/authentication_data.dart
-
4lib/model/mine/user_data.dart
-
2lib/network/api_urls.dart
-
7lib/network/user_api.dart
-
42lib/network/user_api.g.dart
-
109lib/pages/message/chat_page.dart
-
126lib/pages/mine/auth_center_page.dart
-
1lib/pages/mine/login_page.dart
-
196lib/pages/mine/real_name_page.dart
-
425lib/rtc/rtc_manager.dart
-
123lib/widget/message/voice_item.dart
-
32pubspec.lock
-
3pubspec.yaml
@ -0,0 +1,33 @@ |
|||||
|
// ignore_for_file: constant_identifier_names, non_constant_identifier_names |
||||
|
import 'dart:io'; |
||||
|
|
||||
|
import '../model/mine/user_data.dart'; |
||||
|
|
||||
|
class GlobalData { |
||||
|
String? qnToken;//uec接口的Token |
||||
|
String? userId;//用户id |
||||
|
UserData? userData;// 用户的基础信息 |
||||
|
|
||||
|
bool isLogout = false;//是否已经退出登录 |
||||
|
|
||||
|
void logout() { |
||||
|
isLogout = true; |
||||
|
userId = null; |
||||
|
qnToken = null; |
||||
|
userData = null; |
||||
|
} |
||||
|
|
||||
|
static GlobalData getInstance() { |
||||
|
_instance ??= GlobalData._init(); |
||||
|
return _instance!; |
||||
|
} |
||||
|
|
||||
|
GlobalData._init() { |
||||
|
if(Platform.isIOS){ |
||||
|
// xAppId = "503258978847966412"; |
||||
|
} |
||||
|
} |
||||
|
factory GlobalData() => getInstance(); |
||||
|
static GlobalData get instance => getInstance(); |
||||
|
static GlobalData? _instance; |
||||
|
} |
||||
@ -0,0 +1,131 @@ |
|||||
|
import 'dart:async'; |
||||
|
import 'package:get/get.dart'; |
||||
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; |
||||
|
import '../../network/user_api.dart'; |
||||
|
import '../global.dart'; |
||||
|
|
||||
|
class AuthController extends GetxController { |
||||
|
final isLoading = false.obs; |
||||
|
final List<AuthCard> dataList = []; |
||||
|
// 是否正在登录中 |
||||
|
final isLoggingIn = false.obs; |
||||
|
final name = ''.obs; |
||||
|
final idcard = ''.obs; |
||||
|
final agree = false.obs; |
||||
|
// 从GetX依赖注入中获取UserApi实例 |
||||
|
late UserApi _userApi; |
||||
|
@override |
||||
|
void onInit() { |
||||
|
super.onInit(); |
||||
|
// 从全局依赖中获取UserApi |
||||
|
_userApi = Get.find<UserApi>(); |
||||
|
_loadInitialData(); |
||||
|
} |
||||
|
|
||||
|
// 登录方法 |
||||
|
Future<void> _loadInitialData() async { |
||||
|
try { |
||||
|
isLoading.value = true; |
||||
|
late bool realAuth = false; |
||||
|
if(GlobalData().userData?.realAuth != null){ |
||||
|
realAuth = GlobalData().userData!.realAuth!; |
||||
|
} |
||||
|
dataList.assignAll([ |
||||
|
AuthCard( title: '手机绑定', desc: '防止账号丢失', index: 1, authed: true), |
||||
|
AuthCard( title: '真实头像', desc: '提高交友成功率', index: 2, authed: false), |
||||
|
AuthCard( title: '实名认证', desc: '提高交友成功率', index: 3, authed: false), |
||||
|
]); |
||||
|
// 调用登录接口 |
||||
|
// final response = await _userApi.login({}); |
||||
|
// 处理响应 |
||||
|
// if (response.data.isSuccess) { |
||||
|
// |
||||
|
// } |
||||
|
} catch (e) { |
||||
|
SmartDialog.showToast('网络请求失败,请检查网络连接'); |
||||
|
} finally { |
||||
|
isLoading.value = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
bool validateChineseID(String id) { |
||||
|
if (id.length != 18) return false; |
||||
|
|
||||
|
// 系数表 |
||||
|
final coefficients = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]; |
||||
|
|
||||
|
// 校验码对应表 |
||||
|
final checkCodeMap = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']; |
||||
|
|
||||
|
int sum = 0; |
||||
|
|
||||
|
try { |
||||
|
for (int i = 0; i < 17; i++) { |
||||
|
int digit = int.parse(id[i]); |
||||
|
sum += digit * coefficients[i]; |
||||
|
} |
||||
|
} catch (e) { |
||||
|
return false; // 包含非数字字符 |
||||
|
} |
||||
|
|
||||
|
int remainder = sum % 11; |
||||
|
String checkCode = checkCodeMap[remainder]; |
||||
|
|
||||
|
return id[17].toUpperCase() == checkCode; |
||||
|
} |
||||
|
|
||||
|
Future<void> startAuthing() async { |
||||
|
if (name.value.isEmpty) { |
||||
|
SmartDialog.showToast('请输入姓名'); |
||||
|
return; |
||||
|
} |
||||
|
if (idcard.value.isEmpty) { |
||||
|
SmartDialog.showToast('请输入身份证号'); |
||||
|
return; |
||||
|
} |
||||
|
if (!validateChineseID(idcard.value)) { |
||||
|
SmartDialog.showToast('请输入正确的身份证号'); |
||||
|
return; |
||||
|
} |
||||
|
if (!agree.value) { |
||||
|
SmartDialog.showToast('请同意用户认证协议'); |
||||
|
return; |
||||
|
} |
||||
|
try { |
||||
|
// 调用登录接口 |
||||
|
final param = { |
||||
|
'miId': GlobalData().userData?.id, |
||||
|
'authenticationCode': 0, |
||||
|
'value': '${name.value},${idcard.value}', |
||||
|
}; |
||||
|
final response = await _userApi.saveCertificationAudit(param); |
||||
|
// 处理响应 |
||||
|
if (response.data.isSuccess) { |
||||
|
GlobalData().userData?.realAuth = true; |
||||
|
SmartDialog.showToast('认证成功'); |
||||
|
Get.back(result: {'index': 3}); |
||||
|
} else { |
||||
|
SmartDialog.showToast(response.data.message); |
||||
|
} |
||||
|
} catch (e) { |
||||
|
SmartDialog.showToast('网络请求失败,请检查网络连接'); |
||||
|
} finally { |
||||
|
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
class AuthCard { |
||||
|
final String title; |
||||
|
final String desc; |
||||
|
final int index; |
||||
|
final bool authed; |
||||
|
|
||||
|
AuthCard({ |
||||
|
required this.desc, |
||||
|
required this.title, |
||||
|
required this.index, |
||||
|
required this.authed, |
||||
|
}); |
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
class AuthenticationData { |
||||
|
final int? authenticationCode; |
||||
|
final String? authenticationName; |
||||
|
final String? miId; |
||||
|
final int? status; |
||||
|
|
||||
|
AuthenticationData({ |
||||
|
this.authenticationCode, |
||||
|
this.authenticationName, |
||||
|
this.miId, |
||||
|
this.status, |
||||
|
}); |
||||
|
|
||||
|
factory AuthenticationData.fromJson(Map<String, dynamic> json) { |
||||
|
return AuthenticationData( |
||||
|
authenticationCode: json['authenticationCode'] as int?, |
||||
|
authenticationName: json['authenticationName'] as String?, |
||||
|
miId: json['miId'] as String?, |
||||
|
status: json['status'] as int?, |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
Map<String, dynamic> toJson() { |
||||
|
return { |
||||
|
'authenticationCode': authenticationCode, |
||||
|
'authenticationName': authenticationName, |
||||
|
'miId': miId, |
||||
|
'status': status, |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,126 @@ |
|||||
|
import 'package:dating_touchme_app/extension/ex_widget.dart'; |
||||
|
import 'package:dating_touchme_app/pages/mine/real_name_page.dart'; |
||||
|
import 'package:flutter/cupertino.dart'; |
||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
|
||||
|
import '../../controller/mine/auth_controller.dart'; |
||||
|
import 'edit_info_page.dart'; |
||||
|
|
||||
|
class AuthCenterPage extends StatelessWidget { |
||||
|
AuthCenterPage({super.key}); |
||||
|
final AuthController controller = Get.put(AuthController()); |
||||
|
|
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
return Scaffold( |
||||
|
backgroundColor: Color(0xffF5F5F5), |
||||
|
appBar: AppBar( |
||||
|
title: Text('认证中心', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), |
||||
|
centerTitle: true, |
||||
|
leading: IconButton( |
||||
|
icon: Icon(Icons.arrow_back_ios, size: 24, color: Colors.grey,), |
||||
|
onPressed: () { |
||||
|
Get.back(); |
||||
|
}, |
||||
|
), |
||||
|
), |
||||
|
body: Obx(() { |
||||
|
if (controller.isLoading.value) { |
||||
|
return const Center(child: CupertinoActivityIndicator(radius: 12,)); |
||||
|
} |
||||
|
return ListView.builder( |
||||
|
padding: const EdgeInsets.only(top: 16, right: 16, left: 16), |
||||
|
itemCount: controller.dataList.length, |
||||
|
itemBuilder: (context, index) { |
||||
|
final record = controller.dataList[index]; |
||||
|
return _buildListItem(record); |
||||
|
}, |
||||
|
); |
||||
|
}) |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
// 构建列表项 |
||||
|
Widget _buildListItem(AuthCard item) { |
||||
|
return Container( |
||||
|
margin: EdgeInsets.only(bottom: 12), |
||||
|
padding: EdgeInsets.all(24), |
||||
|
decoration: BoxDecoration( |
||||
|
color: Colors.white, |
||||
|
borderRadius: BorderRadius.circular(12), |
||||
|
), |
||||
|
child: Row( |
||||
|
crossAxisAlignment: CrossAxisAlignment.center, |
||||
|
children: [ |
||||
|
// 左侧图片 |
||||
|
Container( |
||||
|
width: 40, |
||||
|
height: 40, |
||||
|
decoration: BoxDecoration( |
||||
|
borderRadius: BorderRadius.circular(8), |
||||
|
color: Colors.blue[100], |
||||
|
image: DecorationImage( |
||||
|
image: NetworkImage('https://picsum.photos/40/40?random=$item.index'), |
||||
|
fit: BoxFit.cover, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
SizedBox(width: 12), |
||||
|
// 右侧内容 |
||||
|
Column( |
||||
|
crossAxisAlignment: CrossAxisAlignment.start, |
||||
|
children: [ |
||||
|
Text( |
||||
|
item.title, |
||||
|
style: TextStyle( |
||||
|
fontSize: 14, |
||||
|
fontWeight: FontWeight.bold, |
||||
|
color: Colors.grey[800], |
||||
|
), |
||||
|
), |
||||
|
SizedBox(height: 2), |
||||
|
Text( |
||||
|
item.desc, |
||||
|
style: TextStyle( |
||||
|
fontSize: 12, |
||||
|
color: Colors.grey[600], |
||||
|
), |
||||
|
maxLines: 1, |
||||
|
overflow: TextOverflow.ellipsis, |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
Spacer(), |
||||
|
Row( |
||||
|
children: [ |
||||
|
Text( |
||||
|
item.authed ? '已认证' : '去认证', |
||||
|
style: TextStyle( |
||||
|
fontSize: 12, |
||||
|
color: item.authed ? Color(0xff26C77C) : Colors.grey[500] |
||||
|
) |
||||
|
), |
||||
|
SizedBox(width: 4), |
||||
|
item.authed ? SizedBox(width: 24) : Icon( |
||||
|
Icons.navigate_next, // Material Icons |
||||
|
// size: 128.0, // 设置图标大小#26C77C |
||||
|
color: Colors.grey[500] |
||||
|
), |
||||
|
], |
||||
|
) |
||||
|
], |
||||
|
), |
||||
|
).onTap(() async{ |
||||
|
if(!item.authed){ |
||||
|
if(item.index == 2){ |
||||
|
Get.to(() => EditInfoPage()); |
||||
|
} else if(item.index == 3){ |
||||
|
final result = await Get.to(() => RealNamePage()); |
||||
|
print(result); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,196 @@ |
|||||
|
import 'package:flutter/cupertino.dart'; |
||||
|
import 'package:flutter/material.dart'; |
||||
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; |
||||
|
import 'package:get/get.dart'; |
||||
|
import 'package:tdesign_flutter/tdesign_flutter.dart'; |
||||
|
|
||||
|
import '../../controller/mine/auth_controller.dart'; |
||||
|
|
||||
|
class RealNamePage extends StatelessWidget { |
||||
|
RealNamePage({super.key}); |
||||
|
final AuthController controller = Get.put(AuthController()); |
||||
|
// 是否同意协议 |
||||
|
@override |
||||
|
Widget build(BuildContext context) { |
||||
|
return Scaffold( |
||||
|
backgroundColor: Color(0xffFFFFFF), |
||||
|
appBar: AppBar( |
||||
|
title: Text('实名认证', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), |
||||
|
centerTitle: true, |
||||
|
leading: IconButton( |
||||
|
icon: Icon(Icons.arrow_back_ios, size: 24, color: Colors.grey,), |
||||
|
onPressed: () { |
||||
|
Get.back(); |
||||
|
}, |
||||
|
), |
||||
|
), |
||||
|
body: Column( |
||||
|
children: [ |
||||
|
Container( |
||||
|
height: 48, |
||||
|
decoration: BoxDecoration(color: Color(0xffE7E7E7)), |
||||
|
padding: const EdgeInsets.only(left: 16), |
||||
|
child: Row( |
||||
|
crossAxisAlignment: CrossAxisAlignment.center, // 垂直居中 |
||||
|
children: [ |
||||
|
Text( |
||||
|
'*请填写本人实名信息', |
||||
|
style: TextStyle( |
||||
|
fontSize: 14, |
||||
|
color: Colors.black87, |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
Container( |
||||
|
height: 56, // 固定高度确保垂直居中 |
||||
|
decoration: BoxDecoration( |
||||
|
border: Border( |
||||
|
bottom: BorderSide( |
||||
|
color: Colors.grey[400]!, |
||||
|
width: 0.5, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
child: Row( |
||||
|
crossAxisAlignment: CrossAxisAlignment.center, // 垂直居中 |
||||
|
children: [ |
||||
|
// 左侧标签 - 固定宽度 + 垂直居中 |
||||
|
Container( |
||||
|
width: 100, |
||||
|
alignment: Alignment.centerLeft, |
||||
|
padding: const EdgeInsets.only(left: 16), |
||||
|
child: Text( |
||||
|
'姓名:', |
||||
|
style: TextStyle( |
||||
|
fontSize: 15, |
||||
|
color: Colors.black87, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
SizedBox(width: 12), |
||||
|
|
||||
|
// 输入框区域 - 使用Expanded填充剩余空间 |
||||
|
Expanded( |
||||
|
child: Container( |
||||
|
alignment: Alignment.centerLeft, // 输入框内容垂直居中 |
||||
|
child: TextField( |
||||
|
decoration: InputDecoration( |
||||
|
hintText: '请输入姓名', |
||||
|
hintStyle: TextStyle(color: Colors.grey[500]), |
||||
|
border: InputBorder.none, // 隐藏默认边框 |
||||
|
contentPadding: EdgeInsets.zero, // 去除默认padding |
||||
|
isDense: true, // 紧凑模式 |
||||
|
), |
||||
|
style: TextStyle( |
||||
|
fontSize: 15, |
||||
|
height: 1.2, // 控制文字垂直位置 |
||||
|
), |
||||
|
onChanged: (value) { |
||||
|
controller.name.value = value; |
||||
|
}, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
// SizedBox(height: 30), |
||||
|
Container( |
||||
|
height: 56, // 固定高度确保垂直居中 |
||||
|
decoration: BoxDecoration( |
||||
|
border: Border( |
||||
|
bottom: BorderSide( |
||||
|
color: Colors.grey[400]!, |
||||
|
width: 0.5, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
child: Row( |
||||
|
crossAxisAlignment: CrossAxisAlignment.center, // 垂直居中 |
||||
|
children: [ |
||||
|
// 左侧标签 - 固定宽度 + 垂直居中 |
||||
|
Container( |
||||
|
width: 100, |
||||
|
alignment: Alignment.centerLeft, |
||||
|
padding: const EdgeInsets.only(left: 16), |
||||
|
child: Text( |
||||
|
'身份证号:', |
||||
|
style: TextStyle( |
||||
|
fontSize: 15, |
||||
|
color: Colors.black87, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
SizedBox(width: 12), |
||||
|
|
||||
|
// 输入框区域 - 使用Expanded填充剩余空间 |
||||
|
Expanded( |
||||
|
child: Container( |
||||
|
alignment: Alignment.centerLeft, // 输入框内容垂直居中 |
||||
|
child: TextField( |
||||
|
decoration: InputDecoration( |
||||
|
hintText: '请输入身份证号', |
||||
|
hintStyle: TextStyle(color: Colors.grey[500]), |
||||
|
border: InputBorder.none, // 隐藏默认边框 |
||||
|
contentPadding: EdgeInsets.zero, // 去除默认padding |
||||
|
isDense: true, // 紧凑模式 |
||||
|
), |
||||
|
style: TextStyle( |
||||
|
fontSize: 15, |
||||
|
height: 1.2, // 控制文字垂直位置 |
||||
|
), |
||||
|
onChanged: (value) { |
||||
|
controller.idcard.value = value; |
||||
|
}, |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
SizedBox(height: 24), |
||||
|
// 协议同意复选框 |
||||
|
Row( |
||||
|
crossAxisAlignment: CrossAxisAlignment.start, // 垂直居中 |
||||
|
children: [ |
||||
|
SizedBox(width: 8), |
||||
|
Obx(() => Checkbox( |
||||
|
value: controller.agree.value, |
||||
|
onChanged: (value) { |
||||
|
controller.agree.value = value ?? false; |
||||
|
}, |
||||
|
activeColor: Color(0xff7562F9), |
||||
|
side: const BorderSide(color: Colors.grey), |
||||
|
shape: const CircleBorder(), |
||||
|
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, |
||||
|
)), |
||||
|
ConstrainedBox( |
||||
|
constraints: BoxConstraints(maxWidth: 300), // 限制最大宽度 |
||||
|
child: Text( |
||||
|
'依据法律法规的要求,我们将收集您的真实姓名,身份证号用于实名认证,认证信息将用于直播连麦、收益提现、依据证件信息更正性别等,与账号唯一绑定,我们会使用加密方式对您的认证信息进行严格保密。', |
||||
|
style: TextStyle( fontSize: 13, color: Colors.grey ), |
||||
|
), |
||||
|
) |
||||
|
], |
||||
|
), |
||||
|
SizedBox(height: 48), |
||||
|
|
||||
|
TDButton( |
||||
|
text: '立即认证', |
||||
|
width: MediaQuery.of(context).size.width - 40, |
||||
|
size: TDButtonSize.large, |
||||
|
type: TDButtonType.fill, |
||||
|
shape: TDButtonShape.round, |
||||
|
theme: TDButtonTheme.primary, |
||||
|
onTap: (){ |
||||
|
controller.startAuthing(); |
||||
|
}, |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,425 @@ |
|||||
|
import 'package:agora_rtc_engine/agora_rtc_engine.dart'; |
||||
|
|
||||
|
/// RTC 管理器,负责管理声网音视频通话功能 |
||||
|
class RTCManager { |
||||
|
// 单例模式 |
||||
|
static final RTCManager _instance = RTCManager._internal(); |
||||
|
factory RTCManager() => _instance; |
||||
|
// 静态getter用于instance访问 |
||||
|
static RTCManager get instance => _instance; |
||||
|
|
||||
|
RtcEngine? _engine; |
||||
|
bool _isInitialized = false; |
||||
|
bool _isInChannel = false; |
||||
|
String? _currentChannelId; |
||||
|
int? _currentUid; |
||||
|
|
||||
|
// 事件回调 |
||||
|
Function(RtcConnection connection, int elapsed)? onJoinChannelSuccess; |
||||
|
Function(RtcConnection connection, int remoteUid, int elapsed)? onUserJoined; |
||||
|
Function( |
||||
|
RtcConnection connection, |
||||
|
int remoteUid, |
||||
|
UserOfflineReasonType reason, |
||||
|
)? |
||||
|
onUserOffline; |
||||
|
Function(RtcConnection connection, RtcStats stats)? onLeaveChannel; |
||||
|
Function( |
||||
|
RtcConnection connection, |
||||
|
ConnectionStateType state, |
||||
|
ConnectionChangedReasonType reason, |
||||
|
)? |
||||
|
onConnectionStateChanged; |
||||
|
Function(int uid, UserInfo userInfo)? onUserInfoUpdated; |
||||
|
Function(RtcConnection connection, int uid, int elapsed)? |
||||
|
onFirstRemoteVideoDecoded; |
||||
|
Function( |
||||
|
RtcConnection connection, |
||||
|
VideoSourceType sourceType, |
||||
|
int uid, |
||||
|
int width, |
||||
|
int height, |
||||
|
int rotation, |
||||
|
)? |
||||
|
onVideoSizeChanged; |
||||
|
Function(RtcConnection connection, int uid, bool muted)? onUserMuteAudio; |
||||
|
Function(RtcConnection connection, int uid, bool muted)? onUserMuteVideo; |
||||
|
Function(RtcConnection connection)? onConnectionLost; |
||||
|
Function(RtcConnection connection, int code, String msg)? onError; |
||||
|
|
||||
|
RTCManager._internal() { |
||||
|
print('RTCManager instance created'); |
||||
|
} |
||||
|
|
||||
|
/// 初始化 RTC Engine |
||||
|
/// [appId] 声网 App ID |
||||
|
/// [channelProfile] 频道场景类型,默认为通信模式 |
||||
|
Future<bool> initialize({ |
||||
|
required String appId, |
||||
|
ChannelProfileType channelProfile = |
||||
|
ChannelProfileType.channelProfileCommunication, |
||||
|
}) async { |
||||
|
try { |
||||
|
if (_isInitialized && _engine != null) { |
||||
|
print('RTC Engine already initialized'); |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
// 创建 RTC Engine |
||||
|
_engine = createAgoraRtcEngine(); |
||||
|
|
||||
|
// 初始化 RTC Engine |
||||
|
await _engine!.initialize( |
||||
|
RtcEngineContext(appId: appId, channelProfile: channelProfile), |
||||
|
); |
||||
|
|
||||
|
// 注册事件处理器 |
||||
|
_registerEventHandlers(); |
||||
|
|
||||
|
_isInitialized = true; |
||||
|
print('RTC Engine initialized successfully'); |
||||
|
return true; |
||||
|
} catch (e) { |
||||
|
print('Failed to initialize RTC Engine: $e'); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// 注册事件处理器 |
||||
|
void _registerEventHandlers() { |
||||
|
if (_engine == null) return; |
||||
|
|
||||
|
_engine!.registerEventHandler( |
||||
|
RtcEngineEventHandler( |
||||
|
onJoinChannelSuccess: (RtcConnection connection, int elapsed) { |
||||
|
_isInChannel = true; |
||||
|
_currentChannelId = connection.channelId; |
||||
|
print('加入频道成功,频道名:${connection.channelId},耗时:${elapsed}ms'); |
||||
|
if (onJoinChannelSuccess != null) { |
||||
|
onJoinChannelSuccess!(connection, elapsed); |
||||
|
} |
||||
|
}, |
||||
|
onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) { |
||||
|
print('用户加入,UID:$remoteUid'); |
||||
|
if (onUserJoined != null) { |
||||
|
onUserJoined!(connection, remoteUid, elapsed); |
||||
|
} |
||||
|
}, |
||||
|
onUserOffline: |
||||
|
( |
||||
|
RtcConnection connection, |
||||
|
int remoteUid, |
||||
|
UserOfflineReasonType reason, |
||||
|
) { |
||||
|
print('用户离开,UID:$remoteUid,原因:$reason'); |
||||
|
if (onUserOffline != null) { |
||||
|
onUserOffline!(connection, remoteUid, reason); |
||||
|
} |
||||
|
}, |
||||
|
onLeaveChannel: (RtcConnection connection, RtcStats stats) { |
||||
|
_isInChannel = false; |
||||
|
_currentChannelId = null; |
||||
|
print('离开频道,统计信息:${stats.duration}秒'); |
||||
|
if (onLeaveChannel != null) { |
||||
|
onLeaveChannel!(connection, stats); |
||||
|
} |
||||
|
}, |
||||
|
onConnectionStateChanged: |
||||
|
( |
||||
|
RtcConnection connection, |
||||
|
ConnectionStateType state, |
||||
|
ConnectionChangedReasonType reason, |
||||
|
) { |
||||
|
print('连接状态改变:$state,原因:$reason'); |
||||
|
if (onConnectionStateChanged != null) { |
||||
|
onConnectionStateChanged!(connection, state, reason); |
||||
|
} |
||||
|
}, |
||||
|
onUserInfoUpdated: (int uid, UserInfo userInfo) { |
||||
|
print('用户信息更新,UID:$uid'); |
||||
|
if (onUserInfoUpdated != null) { |
||||
|
onUserInfoUpdated!(uid, userInfo); |
||||
|
} |
||||
|
}, |
||||
|
onFirstRemoteVideoDecoded: |
||||
|
( |
||||
|
RtcConnection connection, |
||||
|
int uid, |
||||
|
int width, |
||||
|
int height, |
||||
|
int elapsed, |
||||
|
) { |
||||
|
print('首次远程视频解码,UID:$uid,分辨率:${width}x${height}'); |
||||
|
if (onFirstRemoteVideoDecoded != null) { |
||||
|
onFirstRemoteVideoDecoded!(connection, uid, elapsed); |
||||
|
} |
||||
|
}, |
||||
|
onVideoSizeChanged: |
||||
|
( |
||||
|
RtcConnection connection, |
||||
|
VideoSourceType sourceType, |
||||
|
int uid, |
||||
|
int width, |
||||
|
int height, |
||||
|
int rotation, |
||||
|
) { |
||||
|
print('视频尺寸改变,UID:$uid,分辨率:${width}x${height}'); |
||||
|
if (onVideoSizeChanged != null) { |
||||
|
onVideoSizeChanged!( |
||||
|
connection, |
||||
|
sourceType, |
||||
|
uid, |
||||
|
width, |
||||
|
height, |
||||
|
rotation, |
||||
|
); |
||||
|
} |
||||
|
}, |
||||
|
onUserMuteAudio: (RtcConnection connection, int uid, bool muted) { |
||||
|
print('用户静音状态改变,UID:$uid,静音:$muted'); |
||||
|
if (onUserMuteAudio != null) { |
||||
|
onUserMuteAudio!(connection, uid, muted); |
||||
|
} |
||||
|
}, |
||||
|
onUserMuteVideo: (RtcConnection connection, int uid, bool muted) { |
||||
|
print('用户视频状态改变,UID:$uid,关闭:$muted'); |
||||
|
if (onUserMuteVideo != null) { |
||||
|
onUserMuteVideo!(connection, uid, muted); |
||||
|
} |
||||
|
}, |
||||
|
onConnectionLost: (RtcConnection connection) { |
||||
|
print('连接丢失'); |
||||
|
if (onConnectionLost != null) { |
||||
|
onConnectionLost!(connection); |
||||
|
} |
||||
|
}, |
||||
|
onError: (ErrorCodeType err, String msg) { |
||||
|
print('RTC Engine 错误:$err,消息:$msg'); |
||||
|
if (onError != null) { |
||||
|
onError!( |
||||
|
RtcConnection( |
||||
|
channelId: _currentChannelId ?? '', |
||||
|
localUid: _currentUid ?? 0, |
||||
|
), |
||||
|
err.value(), |
||||
|
msg, |
||||
|
); |
||||
|
} |
||||
|
}, |
||||
|
), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
/// 启用视频 |
||||
|
Future<void> enableVideo() async { |
||||
|
if (_engine == null) { |
||||
|
throw Exception('RTC Engine not initialized'); |
||||
|
} |
||||
|
await _engine!.enableVideo(); |
||||
|
print('视频已启用'); |
||||
|
} |
||||
|
|
||||
|
/// 禁用视频 |
||||
|
Future<void> disableVideo() async { |
||||
|
if (_engine == null) { |
||||
|
throw Exception('RTC Engine not initialized'); |
||||
|
} |
||||
|
await _engine!.disableVideo(); |
||||
|
print('视频已禁用'); |
||||
|
} |
||||
|
|
||||
|
/// 启用音频 |
||||
|
Future<void> enableAudio() async { |
||||
|
if (_engine == null) { |
||||
|
throw Exception('RTC Engine not initialized'); |
||||
|
} |
||||
|
await _engine!.enableAudio(); |
||||
|
print('音频已启用'); |
||||
|
} |
||||
|
|
||||
|
/// 禁用音频 |
||||
|
Future<void> disableAudio() async { |
||||
|
if (_engine == null) { |
||||
|
throw Exception('RTC Engine not initialized'); |
||||
|
} |
||||
|
await _engine!.disableAudio(); |
||||
|
print('音频已禁用'); |
||||
|
} |
||||
|
|
||||
|
/// 开启本地视频预览 |
||||
|
Future<void> startPreview() async { |
||||
|
if (_engine == null) { |
||||
|
throw Exception('RTC Engine not initialized'); |
||||
|
} |
||||
|
await _engine!.startPreview(); |
||||
|
print('本地视频预览已开启'); |
||||
|
} |
||||
|
|
||||
|
/// 停止本地视频预览 |
||||
|
Future<void> stopPreview() async { |
||||
|
if (_engine == null) { |
||||
|
throw Exception('RTC Engine not initialized'); |
||||
|
} |
||||
|
await _engine!.stopPreview(); |
||||
|
print('本地视频预览已停止'); |
||||
|
} |
||||
|
|
||||
|
/// 设置本地视频视图 |
||||
|
/// [viewId] 视图ID |
||||
|
/// [mirrorMode] 镜像模式 |
||||
|
Future<void> setupLocalVideo({ |
||||
|
required int viewId, |
||||
|
VideoSourceType sourceType = VideoSourceType.videoSourceCameraPrimary, |
||||
|
VideoMirrorModeType mirrorMode = VideoMirrorModeType.videoMirrorModeAuto, |
||||
|
}) async { |
||||
|
if (_engine == null) { |
||||
|
throw Exception('RTC Engine not initialized'); |
||||
|
} |
||||
|
await _engine!.setupLocalVideo( |
||||
|
VideoCanvas(view: viewId, sourceType: sourceType, mirrorMode: mirrorMode), |
||||
|
); |
||||
|
print('本地视频视图已设置,viewId:$viewId'); |
||||
|
} |
||||
|
|
||||
|
/// 设置远程视频视图 |
||||
|
/// [uid] 远程用户ID |
||||
|
/// [viewId] 视图ID |
||||
|
Future<void> setupRemoteVideo({ |
||||
|
required int uid, |
||||
|
required int viewId, |
||||
|
VideoSourceType sourceType = VideoSourceType.videoSourceCameraPrimary, |
||||
|
VideoMirrorModeType mirrorMode = |
||||
|
VideoMirrorModeType.videoMirrorModeDisabled, |
||||
|
}) async { |
||||
|
if (_engine == null) { |
||||
|
throw Exception('RTC Engine not initialized'); |
||||
|
} |
||||
|
await _engine!.setupRemoteVideo( |
||||
|
VideoCanvas( |
||||
|
uid: uid, |
||||
|
view: viewId, |
||||
|
sourceType: sourceType, |
||||
|
mirrorMode: mirrorMode, |
||||
|
), |
||||
|
); |
||||
|
print('远程视频视图已设置,UID:$uid,viewId:$viewId'); |
||||
|
} |
||||
|
|
||||
|
/// 加入频道 |
||||
|
/// [token] 频道令牌(可选,如果频道未开启鉴权则可以为空字符串) |
||||
|
/// [channelId] 频道ID |
||||
|
/// [uid] 用户ID(0表示自动分配) |
||||
|
/// [options] 频道媒体选项 |
||||
|
Future<void> joinChannel({ |
||||
|
String? token, |
||||
|
required String channelId, |
||||
|
int uid = 0, |
||||
|
ChannelMediaOptions? options, |
||||
|
}) async { |
||||
|
if (_engine == null) { |
||||
|
throw Exception('RTC Engine not initialized'); |
||||
|
} |
||||
|
if (_isInChannel) { |
||||
|
print('已经在频道中,先离开当前频道'); |
||||
|
await leaveChannel(); |
||||
|
} |
||||
|
|
||||
|
_currentUid = uid; |
||||
|
await _engine!.joinChannel( |
||||
|
token: token ?? '', |
||||
|
channelId: channelId, |
||||
|
uid: uid, |
||||
|
options: options ?? const ChannelMediaOptions(), |
||||
|
); |
||||
|
print('正在加入频道:$channelId,UID:$uid'); |
||||
|
} |
||||
|
|
||||
|
/// 离开频道 |
||||
|
Future<void> leaveChannel() async { |
||||
|
if (_engine == null) { |
||||
|
throw Exception('RTC Engine not initialized'); |
||||
|
} |
||||
|
if (!_isInChannel) { |
||||
|
print('当前不在频道中'); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
await _engine!.leaveChannel(); |
||||
|
_currentUid = null; |
||||
|
print('已离开频道'); |
||||
|
} |
||||
|
|
||||
|
/// 切换摄像头 |
||||
|
Future<void> switchCamera() async { |
||||
|
if (_engine == null) { |
||||
|
throw Exception('RTC Engine not initialized'); |
||||
|
} |
||||
|
await _engine!.switchCamera(); |
||||
|
print('摄像头已切换'); |
||||
|
} |
||||
|
|
||||
|
/// 静音/取消静音本地音频 |
||||
|
/// [muted] true表示静音,false表示取消静音 |
||||
|
Future<void> muteLocalAudio(bool muted) async { |
||||
|
if (_engine == null) { |
||||
|
throw Exception('RTC Engine not initialized'); |
||||
|
} |
||||
|
await _engine!.muteLocalAudioStream(muted); |
||||
|
print('本地音频${muted ? "已静音" : "已取消静音"}'); |
||||
|
} |
||||
|
|
||||
|
/// 开启/关闭本地视频 |
||||
|
/// [enabled] true表示开启,false表示关闭 |
||||
|
Future<void> muteLocalVideo(bool enabled) async { |
||||
|
if (_engine == null) { |
||||
|
throw Exception('RTC Engine not initialized'); |
||||
|
} |
||||
|
await _engine!.muteLocalVideoStream(!enabled); |
||||
|
print('本地视频${enabled ? "已开启" : "已关闭"}'); |
||||
|
} |
||||
|
|
||||
|
/// 设置客户端角色(仅用于直播场景) |
||||
|
/// [role] 客户端角色:主播或观众 |
||||
|
Future<void> setClientRole({ |
||||
|
required ClientRoleType role, |
||||
|
ClientRoleOptions? options, |
||||
|
}) async { |
||||
|
if (_engine == null) { |
||||
|
throw Exception('RTC Engine not initialized'); |
||||
|
} |
||||
|
await _engine!.setClientRole(role: role, options: options); |
||||
|
print('客户端角色已设置为:$role'); |
||||
|
} |
||||
|
|
||||
|
/// 获取当前是否在频道中 |
||||
|
bool get isInChannel => _isInChannel; |
||||
|
|
||||
|
/// 获取当前频道ID |
||||
|
String? get currentChannelId => _currentChannelId; |
||||
|
|
||||
|
/// 获取当前用户ID |
||||
|
int? get currentUid => _currentUid; |
||||
|
|
||||
|
/// 获取 RTC Engine 实例(用于高级操作) |
||||
|
RtcEngine? get engine => _engine; |
||||
|
|
||||
|
/// 释放资源 |
||||
|
Future<void> dispose() async { |
||||
|
try { |
||||
|
if (_isInChannel) { |
||||
|
await leaveChannel(); |
||||
|
} |
||||
|
if (_engine != null) { |
||||
|
await _engine!.release(); |
||||
|
_engine = null; |
||||
|
} |
||||
|
_isInitialized = false; |
||||
|
_isInChannel = false; |
||||
|
_currentChannelId = null; |
||||
|
_currentUid = null; |
||||
|
print('RTC Engine disposed'); |
||||
|
} catch (e) { |
||||
|
print('Failed to dispose RTC Engine: $e'); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save