Browse Source

feat(rtc): 实现RTC频道连接与用户角色管理功能

- 新增RTC频道连接、断开、用户详情获取等相关API接口
- 在RoomController中增加CurrentRole枚举及角色管理逻辑
- 实现观众加入连麦功能,支持不同性别用户进入不同席位
- 扩展聊天面板UI,根据用户角色动态显示连麦入口
- 增加RTC管理器发布音视频流的功能方法
- 调整聊天消息最大存储数量从100条增至300条
- 删除冗余的sendMessage旧方法定义
ios
Jolie 4 months ago
parent
commit
405a58aacb
6 changed files with 289 additions and 49 deletions
  1. 40
      lib/controller/discover/room_controller.dart
  2. 8
      lib/network/api_urls.dart
  3. 25
      lib/network/rtc_api.dart
  4. 149
      lib/network/rtc_api.g.dart
  5. 30
      lib/rtc/rtc_manager.dart
  6. 86
      lib/widget/live/live_room_notice_chat_panel.dart

40
lib/controller/discover/room_controller.dart

@ -9,13 +9,22 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart';
//
enum CurrentRole{
broadcaster,//
maleAudience,//
femaleAudience,//
audience,//
normalUser, //
}
///
class RoomController extends GetxController {
RoomController({NetworkService? networkService})
: _networkService = networkService ?? Get.find<NetworkService>();
final NetworkService _networkService;
CurrentRole currentRole = CurrentRole.normalUser;
///
final Rxn<RtcChannelData> rtcChannel = Rxn<RtcChannelData>();
final Rxn<RtcChannelDetail> rtcChannelDetail = Rxn<RtcChannelDetail>();
@ -72,7 +81,7 @@ class RoomController extends GetxController {
print('✅ 消息已添加到列表,当前消息数: ${chatMessages.length}');
// 100
if (chatMessages.length > 100) {
if (chatMessages.length > 300) {
chatMessages.removeAt(0);
print('📝 消息列表已满,移除最旧的消息');
}
@ -88,6 +97,7 @@ class RoomController extends GetxController {
final base = response.data;
if (base.isSuccess && base.data != null) {
rtcChannel.value = base.data;
currentRole = CurrentRole.broadcaster;
await _joinRtcChannel(
base.data!.token,
base.data!.channelId,
@ -109,6 +119,7 @@ class RoomController extends GetxController {
final base = response.data;
if (base.isSuccess && base.data != null) {
rtcChannel.value = base.data;
currentRole = CurrentRole.normalUser;
await _joinRtcChannel(
base.data!.token,
channelName,
@ -140,6 +151,26 @@ class RoomController extends GetxController {
}
}
Future<void> joinChat(CurrentRole role) async {
final data = {
'channelId': rtcChannel.value?.channelId,
'seatNumber': role == CurrentRole.maleAudience ? 1 : 2,
'isMicrophoneOn': role != CurrentRole.normalUser ? true : false,
'isVideoOn': role == CurrentRole.maleAudience || role == CurrentRole.femaleAudience ? true : false,
};
final response = await _networkService.rtcApi.connectRtcChannel(data);
if (!response.data.isSuccess) {
SmartDialog.showToast(response.data.message);
return;
}
currentRole = role;
if(role == CurrentRole.maleAudience || role == CurrentRole.femaleAudience){
await RTCManager.instance.publishVideo(role);
}else{
await RTCManager.instance.publishAudio();
}
}
Future<void> _fetchRtcChannelDetail(String channelName) async {
try {
final response = await _networkService.rtcApi.getRtcChannelDetail(
@ -170,11 +201,6 @@ class RoomController extends GetxController {
}
}
///
Future<void> sendMessage(String message) async {
await sendChatMessage(message);
}
Future<bool> _ensureRtcPermissions() async {
final statuses = await [Permission.camera, Permission.microphone].request();
final allGranted = statuses.values.every((status) => status.isGranted);

8
lib/network/api_urls.dart

@ -48,6 +48,14 @@ class ApiUrls {
'dating-agency-chat-audio/user/get/sw/rtm/token';
static const String getRtcChannelDetail =
'dating-agency-chat-audio/user/get/dating-rtc-channel/detail';
static const String connectRtcChannel =
'dating-agency-chat-audio/user/connect/rtc-channel';
static const String getDatingRtcChannelUserDetail =
'dating-agency-chat-audio/user/get/dating-rtc-channel-user/detail';
static const String enableRtcChannelUserAudio =
'dating-agency-chat-audio/user/enable/rtc-channel-user/audio';
static const String disconnectRtcChannel =
'dating-agency-chat-audio/user/disconnect/rtc-channel';
static const String listBankCardByIndividual =
'dating-agency-mall/user/list/bank-card/by-individual';
static const String createBankCardByIndividual =

25
lib/network/rtc_api.dart

@ -31,4 +31,29 @@ abstract class RtcApi {
Future<HttpResponse<BaseResponse<RtcChannelDetail>>> getRtcChannelDetail(
@Query('channelId') String channelId,
);
/// RTC
@POST(ApiUrls.connectRtcChannel)
Future<HttpResponse<BaseResponse<dynamic>>> connectRtcChannel(
@Body() Map<String, dynamic> data,
);
/// RTC
@GET(ApiUrls.getDatingRtcChannelUserDetail)
Future<HttpResponse<BaseResponse<dynamic>>> getDatingRtcChannelUserDetail(
@Query('channelId') String channelId,
@Query('uId') String uId,
);
/// / RTC
@POST(ApiUrls.enableRtcChannelUserAudio)
Future<HttpResponse<BaseResponse<dynamic>>> enableRtcChannelUserAudio(
@Body() Map<String, dynamic> data,
);
/// RTC
@POST(ApiUrls.disconnectRtcChannel)
Future<HttpResponse<BaseResponse<dynamic>>> disconnectRtcChannel(
@Body() Map<String, dynamic> data,
);
}

149
lib/network/rtc_api.g.dart

@ -124,21 +124,162 @@ class _RtcApi implements RtcApi {
const Map<String, dynamic>? _data = null;
final _options =
_setStreamType<HttpResponse<BaseResponse<RtcChannelDetail>>>(
Options(method: 'GET', headers: _headers, extra: _extra)
.compose(
_dio.options,
'dating-agency-chat-audio/user/get/dating-rtc-channel/detail',
queryParameters: queryParameters,
data: _data,
)
.copyWith(
baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl),
),
);
final _result = await _dio.fetch<Map<String, dynamic>>(_options);
late BaseResponse<RtcChannelDetail> _value;
try {
_value = BaseResponse<RtcChannelDetail>.fromJson(
_result.data!,
(json) => RtcChannelDetail.fromJson(json as Map<String, dynamic>),
);
} on Object catch (e, s) {
errorLogger?.logError(e, s, _options);
rethrow;
}
final httpResponse = HttpResponse(_value, _result);
return httpResponse;
}
@override
Future<HttpResponse<BaseResponse<dynamic>>> connectRtcChannel(
Map<String, dynamic> data,
) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final _data = <String, dynamic>{};
_data.addAll(data);
final _options = _setStreamType<HttpResponse<BaseResponse<dynamic>>>(
Options(method: 'POST', headers: _headers, extra: _extra)
.compose(
_dio.options,
'dating-agency-chat-audio/user/connect/rtc-channel',
queryParameters: queryParameters,
data: _data,
)
.copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)),
);
final _result = await _dio.fetch<Map<String, dynamic>>(_options);
late BaseResponse<dynamic> _value;
try {
_value = BaseResponse<dynamic>.fromJson(
_result.data!,
(json) => json as dynamic,
);
} on Object catch (e, s) {
errorLogger?.logError(e, s, _options);
rethrow;
}
final httpResponse = HttpResponse(_value, _result);
return httpResponse;
}
@override
Future<HttpResponse<BaseResponse<dynamic>>> getDatingRtcChannelUserDetail(
String channelId,
String uId,
) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{
r'channelId': channelId,
r'uId': uId,
};
final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null;
final _options = _setStreamType<HttpResponse<BaseResponse<dynamic>>>(
Options(method: 'GET', headers: _headers, extra: _extra)
.compose(
_dio.options,
'dating-agency-chat-audio/user/get/dating-rtc-channel/detail',
'dating-agency-chat-audio/user/get/dating-rtc-channel-user/detail',
queryParameters: queryParameters,
data: _data,
)
.copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)),
);
final _result = await _dio.fetch<Map<String, dynamic>>(_options);
late BaseResponse<RtcChannelDetail> _value;
late BaseResponse<dynamic> _value;
try {
_value = BaseResponse<RtcChannelDetail>.fromJson(
_value = BaseResponse<dynamic>.fromJson(
_result.data!,
(json) => RtcChannelDetail.fromJson(json as Map<String, dynamic>),
(json) => json as dynamic,
);
} on Object catch (e, s) {
errorLogger?.logError(e, s, _options);
rethrow;
}
final httpResponse = HttpResponse(_value, _result);
return httpResponse;
}
@override
Future<HttpResponse<BaseResponse<dynamic>>> enableRtcChannelUserAudio(
Map<String, dynamic> data,
) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final _data = <String, dynamic>{};
_data.addAll(data);
final _options = _setStreamType<HttpResponse<BaseResponse<dynamic>>>(
Options(method: 'POST', headers: _headers, extra: _extra)
.compose(
_dio.options,
'dating-agency-chat-audio/user/enable/rtc-channel-user/audio',
queryParameters: queryParameters,
data: _data,
)
.copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)),
);
final _result = await _dio.fetch<Map<String, dynamic>>(_options);
late BaseResponse<dynamic> _value;
try {
_value = BaseResponse<dynamic>.fromJson(
_result.data!,
(json) => json as dynamic,
);
} on Object catch (e, s) {
errorLogger?.logError(e, s, _options);
rethrow;
}
final httpResponse = HttpResponse(_value, _result);
return httpResponse;
}
@override
Future<HttpResponse<BaseResponse<dynamic>>> disconnectRtcChannel(
Map<String, dynamic> data,
) async {
final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{};
final _data = <String, dynamic>{};
_data.addAll(data);
final _options = _setStreamType<HttpResponse<BaseResponse<dynamic>>>(
Options(method: 'POST', headers: _headers, extra: _extra)
.compose(
_dio.options,
'dating-agency-chat-audio/user/disconnect/rtc-channel',
queryParameters: queryParameters,
data: _data,
)
.copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)),
);
final _result = await _dio.fetch<Map<String, dynamic>>(_options);
late BaseResponse<dynamic> _value;
try {
_value = BaseResponse<dynamic>.fromJson(
_result.data!,
(json) => json as dynamic,
);
} on Object catch (e, s) {
errorLogger?.logError(e, s, _options);

30
lib/rtc/rtc_manager.dart

@ -5,6 +5,7 @@ import 'package:dating_touchme_app/rtc/rtm_manager.dart';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import '../controller/discover/room_controller.dart';
import '../pages/discover/live_room_page.dart';
/// RTC
@ -480,4 +481,33 @@ class RTCManager {
if (!removed) return;
remoteUsersNotifier.value = List<int>.unmodifiable(_remoteUserIds);
}
///
Future<void> publishVideo(CurrentRole role) async {
await _engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
await _engine?.muteLocalAudioStream(false);
await _engine?.muteLocalVideoStream(false);
await RTMManager.instance.publishChannelMessage(
channelName: _currentChannelId ?? '',
message: json.encode({
'type': 'join_chat',
'uid': _currentUid,
'role': role == CurrentRole.maleAudience ? 'male_audience' : 'female_audience'
}),
);
}
Future<void> publishAudio() async {
await _engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
await _engine?.muteLocalAudioStream(false);
await _engine?.muteLocalVideoStream(true);
await RTMManager.instance.publishChannelMessage(
channelName: _currentChannelId ?? '',
message: json.encode({
'type': 'join_chat',
'uid': _currentUid,
'role': 'audience'
}),
);
}
}

86
lib/widget/live/live_room_notice_chat_panel.dart

@ -1,4 +1,6 @@
import 'package:dating_touchme_app/controller/discover/room_controller.dart';
import 'package:dating_touchme_app/controller/global.dart';
import 'package:dating_touchme_app/extension/ex_widget.dart';
import 'package:dating_touchme_app/generated/assets.dart';
import 'package:dating_touchme_app/widget/live/live_room_chat_item.dart';
import 'package:flutter/material.dart';
@ -73,50 +75,58 @@ class _LiveRoomNoticeChatPanelState extends State<LiveRoomNoticeChatPanel> {
}),
),
SizedBox(width: 18.w),
Container(
width: 120.w,
height: 55.w,
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 10.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.w),
gradient: const LinearGradient(
colors: [Color(0xFF7C63FF), Color(0xFF987CFF)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Row(
children: [
Image.asset(
Assets.imagesRoomVideo,
width: 26.w,
Obx((){
if(controller.rtcChannelDetail.value?.maleInfo == null && GlobalData().userData?.genderCode == 1 && controller.currentRole != CurrentRole.broadcaster ||
controller.rtcChannelDetail.value?.femaleInfo == null && GlobalData().userData?.genderCode == 2 && controller.currentRole != CurrentRole.broadcaster){
return Container(
width: 120.w,
height: 55.w,
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 10.w),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.w),
gradient: const LinearGradient(
colors: [Color(0xFF7C63FF), Color(0xFF987CFF)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
SizedBox(width: 8.w),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
child: Row(
children: [
Text(
'免费连麦',
style: TextStyle(
fontSize: 13.w,
color: Colors.white,
fontWeight: FontWeight.w600,
),
Image.asset(
Assets.imagesRoomVideo,
width: 26.w,
),
SizedBox(height: 2.w),
Text(
'剩余2张相亲卡',
style: TextStyle(
fontSize: 9.w,
color: Colors.white.withOpacity(0.8),
),
SizedBox(width: 8.w),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'免费连麦',
style: TextStyle(
fontSize: 13.w,
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
SizedBox(height: 2.w),
Text(
'剩余2张相亲卡',
style: TextStyle(
fontSize: 9.w,
color: Colors.white.withOpacity(0.8),
),
),
],
),
],
),
],
),
),
).onTap(() async{
await controller.joinChat(GlobalData().userData?.genderCode == 1 ? CurrentRole.maleAudience : CurrentRole.femaleAudience);
});
}
return const SizedBox();
}),
],
),
);

Loading…
Cancel
Save