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
Split 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