From 405a58aacb1b28ee65004292f39dee1428411f38 Mon Sep 17 00:00:00 2001 From: Jolie <412895109@qq.com> Date: Sun, 23 Nov 2025 20:15:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(rtc):=20=E5=AE=9E=E7=8E=B0RTC=E9=A2=91?= =?UTF-8?q?=E9=81=93=E8=BF=9E=E6=8E=A5=E4=B8=8E=E7=94=A8=E6=88=B7=E8=A7=92?= =?UTF-8?q?=E8=89=B2=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增RTC频道连接、断开、用户详情获取等相关API接口 - 在RoomController中增加CurrentRole枚举及角色管理逻辑 - 实现观众加入连麦功能,支持不同性别用户进入不同席位 - 扩展聊天面板UI,根据用户角色动态显示连麦入口 - 增加RTC管理器发布音视频流的功能方法 - 调整聊天消息最大存储数量从100条增至300条 - 删除冗余的sendMessage旧方法定义 --- lib/controller/discover/room_controller.dart | 40 ++++- lib/network/api_urls.dart | 8 + lib/network/rtc_api.dart | 25 +++ lib/network/rtc_api.g.dart | 149 +++++++++++++++++- lib/rtc/rtc_manager.dart | 30 ++++ .../live/live_room_notice_chat_panel.dart | 86 +++++----- 6 files changed, 289 insertions(+), 49 deletions(-) diff --git a/lib/controller/discover/room_controller.dart b/lib/controller/discover/room_controller.dart index 031e2b6..7fd2931 100644 --- a/lib/controller/discover/room_controller.dart +++ b/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(); final NetworkService _networkService; - + CurrentRole currentRole = CurrentRole.normalUser; /// 当前频道信息 final Rxn rtcChannel = Rxn(); final Rxn rtcChannelDetail = Rxn(); @@ -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 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 _fetchRtcChannelDetail(String channelName) async { try { final response = await _networkService.rtcApi.getRtcChannelDetail( @@ -170,11 +201,6 @@ class RoomController extends GetxController { } } - /// 发送消息(保留原有方法,用于兼容) - Future sendMessage(String message) async { - await sendChatMessage(message); - } - Future _ensureRtcPermissions() async { final statuses = await [Permission.camera, Permission.microphone].request(); final allGranted = statuses.values.every((status) => status.isGranted); diff --git a/lib/network/api_urls.dart b/lib/network/api_urls.dart index e74c38b..d707a22 100644 --- a/lib/network/api_urls.dart +++ b/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 = diff --git a/lib/network/rtc_api.dart b/lib/network/rtc_api.dart index da3511f..f68905f 100644 --- a/lib/network/rtc_api.dart +++ b/lib/network/rtc_api.dart @@ -31,4 +31,29 @@ abstract class RtcApi { Future>> getRtcChannelDetail( @Query('channelId') String channelId, ); + + /// 连接 RTC 频道 + @POST(ApiUrls.connectRtcChannel) + Future>> connectRtcChannel( + @Body() Map data, + ); + + /// 获取 RTC 频道用户详情 + @GET(ApiUrls.getDatingRtcChannelUserDetail) + Future>> getDatingRtcChannelUserDetail( + @Query('channelId') String channelId, + @Query('uId') String uId, + ); + + /// 启用/禁用 RTC 频道用户音频 + @POST(ApiUrls.enableRtcChannelUserAudio) + Future>> enableRtcChannelUserAudio( + @Body() Map data, + ); + + /// 断开 RTC 频道连接 + @POST(ApiUrls.disconnectRtcChannel) + Future>> disconnectRtcChannel( + @Body() Map data, + ); } diff --git a/lib/network/rtc_api.g.dart b/lib/network/rtc_api.g.dart index d1b22db..de382ed 100644 --- a/lib/network/rtc_api.g.dart +++ b/lib/network/rtc_api.g.dart @@ -124,21 +124,162 @@ class _RtcApi implements RtcApi { const Map? _data = null; final _options = _setStreamType>>( + 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>(_options); + late BaseResponse _value; + try { + _value = BaseResponse.fromJson( + _result.data!, + (json) => RtcChannelDetail.fromJson(json as Map), + ); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); + return httpResponse; + } + + @override + Future>> connectRtcChannel( + Map data, + ) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(data); + final _options = _setStreamType>>( + 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>(_options); + late BaseResponse _value; + try { + _value = BaseResponse.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>> getDatingRtcChannelUserDetail( + String channelId, + String uId, + ) async { + final _extra = {}; + final queryParameters = { + r'channelId': channelId, + r'uId': uId, + }; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType>>( 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>(_options); - late BaseResponse _value; + late BaseResponse _value; try { - _value = BaseResponse.fromJson( + _value = BaseResponse.fromJson( _result.data!, - (json) => RtcChannelDetail.fromJson(json as Map), + (json) => json as dynamic, + ); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + final httpResponse = HttpResponse(_value, _result); + return httpResponse; + } + + @override + Future>> enableRtcChannelUserAudio( + Map data, + ) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(data); + final _options = _setStreamType>>( + 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>(_options); + late BaseResponse _value; + try { + _value = BaseResponse.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>> disconnectRtcChannel( + Map data, + ) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + final _data = {}; + _data.addAll(data); + final _options = _setStreamType>>( + 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>(_options); + late BaseResponse _value; + try { + _value = BaseResponse.fromJson( + _result.data!, + (json) => json as dynamic, ); } on Object catch (e, s) { errorLogger?.logError(e, s, _options); diff --git a/lib/rtc/rtc_manager.dart b/lib/rtc/rtc_manager.dart index a88da39..4607895 100644 --- a/lib/rtc/rtc_manager.dart +++ b/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.unmodifiable(_remoteUserIds); } + + /// 发布视频 + Future 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 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' + }), + ); + } } diff --git a/lib/widget/live/live_room_notice_chat_panel.dart b/lib/widget/live/live_room_notice_chat_panel.dart index f5be64f..bf574f5 100644 --- a/lib/widget/live/live_room_notice_chat_panel.dart +++ b/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 { }), ), 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(); + }), ], ), );