Browse Source

feat(live): 实现直播间连麦功能及RTC消息处理

- 添加对 RTC 消息类型的扩展支持,包括 join_chat 和 leave_chat
- 更新 RoomController 以处理连麦用户的加入与离开逻辑
- 修改 UI 组件以反映连麦状态变化(如按钮文字、颜色)
- 调整 RTM 消息解析流程,将未知类型消息路由至 RoomController 处理
- 移除调试日志并优化代码结构和可读性
- 修正网络请求返回值类型,确保数据模型一致性
- 增强 RTC 引擎用户加入事件的异步处理能力
ios
Jolie 4 months ago
parent
commit
1f39ba928b
7 changed files with 155 additions and 44 deletions
  1. 136
      lib/controller/discover/room_controller.dart
  2. 4
      lib/network/rtc_api.dart
  3. 14
      lib/network/rtc_api.g.dart
  4. 18
      lib/rtc/rtc_manager.dart
  5. 9
      lib/service/live_chat_message_service.dart
  6. 6
      lib/widget/live/live_room_anchor_showcase.dart
  7. 12
      lib/widget/live/live_room_notice_chat_panel.dart

136
lib/controller/discover/room_controller.dart

@ -1,5 +1,4 @@
import 'package:agora_rtc_engine/agora_rtc_engine.dart'; import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'package:dating_touchme_app/model/live/live_chat_message.dart';
import 'package:dating_touchme_app/model/rtc/rtc_channel_data.dart'; import 'package:dating_touchme_app/model/rtc/rtc_channel_data.dart';
import 'package:dating_touchme_app/model/rtc/rtc_channel_detail.dart'; import 'package:dating_touchme_app/model/rtc/rtc_channel_detail.dart';
import 'package:dating_touchme_app/network/network_service.dart'; import 'package:dating_touchme_app/network/network_service.dart';
@ -9,12 +8,14 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import '../../model/live/live_chat_message.dart';
// //
enum CurrentRole{
broadcaster,//
maleAudience,//
femaleAudience,//
audience,//
enum CurrentRole {
broadcaster, //
maleAudience, //
femaleAudience, //
audience, //
normalUser, // normalUser, //
} }
@ -26,6 +27,7 @@ class RoomController extends GetxController {
final NetworkService _networkService; final NetworkService _networkService;
CurrentRole currentRole = CurrentRole.normalUser; CurrentRole currentRole = CurrentRole.normalUser;
bool isLive = false; bool isLive = false;
/// ///
final Rxn<RtcChannelData> rtcChannel = Rxn<RtcChannelData>(); final Rxn<RtcChannelData> rtcChannel = Rxn<RtcChannelData>();
final Rxn<RtcChannelDetail> rtcChannelDetail = Rxn<RtcChannelDetail>(); final Rxn<RtcChannelDetail> rtcChannelDetail = Rxn<RtcChannelDetail>();
@ -158,7 +160,10 @@ class RoomController extends GetxController {
'channelId': rtcChannel.value?.channelId, 'channelId': rtcChannel.value?.channelId,
'seatNumber': role == CurrentRole.maleAudience ? 1 : 2, 'seatNumber': role == CurrentRole.maleAudience ? 1 : 2,
'isMicrophoneOn': role != CurrentRole.normalUser ? true : false, 'isMicrophoneOn': role != CurrentRole.normalUser ? true : false,
'isVideoOn': role == CurrentRole.maleAudience || role == CurrentRole.femaleAudience ? true : false,
'isVideoOn':
role == CurrentRole.maleAudience || role == CurrentRole.femaleAudience
? true
: false,
}; };
final response = await _networkService.rtcApi.connectRtcChannel(data); final response = await _networkService.rtcApi.connectRtcChannel(data);
if (!response.data.isSuccess) { if (!response.data.isSuccess) {
@ -166,21 +171,38 @@ class RoomController extends GetxController {
return; return;
} }
currentRole = role; currentRole = role;
if(role == CurrentRole.maleAudience || role == CurrentRole.femaleAudience){
if (role == CurrentRole.maleAudience ||
role == CurrentRole.femaleAudience) {
await RTCManager.instance.publishVideo(role); await RTCManager.instance.publishVideo(role);
}else{
} else {
await RTCManager.instance.publishAudio(); await RTCManager.instance.publishAudio();
} }
isLive = true; isLive = true;
} }
Future<void> leaveChat() async { Future<void> leaveChat() async {
final data = {
'channelId': rtcChannel.value?.channelId
};
final data = {'channelId': rtcChannel.value?.channelId};
final response = await _networkService.rtcApi.disconnectRtcChannel(data); final response = await _networkService.rtcApi.disconnectRtcChannel(data);
if(response.data.isSuccess){
if (response.data.isSuccess) {
isLive = false;
await RTCManager.instance.unpublish(); await RTCManager.instance.unpublish();
if (currentRole == CurrentRole.maleAudience) {
final newDetail = RtcChannelDetail(
channelId: rtcChannelDetail.value!.channelId,
anchorInfo: rtcChannelDetail.value!.anchorInfo,
maleInfo: null,
femaleInfo: rtcChannelDetail.value!.femaleInfo,
);
rtcChannelDetail.value = newDetail;
} else if (currentRole == CurrentRole.femaleAudience) {
final newDetail = RtcChannelDetail(
channelId: rtcChannelDetail.value!.channelId,
anchorInfo: rtcChannelDetail.value!.anchorInfo,
maleInfo: rtcChannelDetail.value!.maleInfo,
femaleInfo: null,
);
rtcChannelDetail.value = newDetail;
}
} }
} }
@ -234,6 +256,94 @@ class RoomController extends GetxController {
} }
Future<void> leaveChannel() async { Future<void> leaveChannel() async {
isLive = false;
currentRole = CurrentRole.normalUser;;
await RTCManager.instance.leaveChannel(); await RTCManager.instance.leaveChannel();
} }
/// RTC消息
Future<void> receiveRTCMessage(Map<String, dynamic> message) async {
if (message['type'] == 'join_chat') {
final response = await _networkService.rtcApi.getDatingRtcChannelUserDetail(
rtcChannel.value!.channelId,
message['uid'],
);
if (!response.data.isSuccess) {
return;
}
final currentDetail = rtcChannelDetail.value;
if (currentDetail == null) {
return;
}
if (message['role'] == 'male_audience') {
final userData = response.data.data;
// if (userData != null) {
final maleInfo = RtcSeatUserInfo(
miId: rtcChannelDetail.value!.anchorInfo!.miId,
userId: rtcChannelDetail.value!.anchorInfo!.userId,
nickName: rtcChannelDetail.value!.anchorInfo!.nickName,
profilePhoto: rtcChannelDetail.value!.anchorInfo!.profilePhoto,
genderCode: rtcChannelDetail.value!.anchorInfo!.genderCode,
seatNumber: rtcChannelDetail.value!.anchorInfo!.seatNumber,
isFriend: rtcChannelDetail.value!.anchorInfo!.isFriend,
isMicrophoneOn: rtcChannelDetail.value!.anchorInfo!.isMicrophoneOn,
isVideoOn: rtcChannelDetail.value!.anchorInfo!.isVideoOn,
uid: message['uid'] is int
? message['uid'] as int
: int.tryParse(message['uid']?.toString() ?? ''),
);
final newDetail = RtcChannelDetail(
channelId: currentDetail.channelId,
anchorInfo: currentDetail.anchorInfo,
maleInfo: maleInfo,
femaleInfo: currentDetail.femaleInfo,
);
rtcChannelDetail.value = newDetail;
// }
} else if (message['role'] == 'female_audience') {
final userData = response.data.data;
// if (userData != null) {
final femaleInfo = RtcSeatUserInfo(
miId: rtcChannelDetail.value!.anchorInfo!.miId,
userId: rtcChannelDetail.value!.anchorInfo!.userId,
nickName: rtcChannelDetail.value!.anchorInfo!.nickName,
profilePhoto: rtcChannelDetail.value!.anchorInfo!.profilePhoto,
genderCode: rtcChannelDetail.value!.anchorInfo!.genderCode,
seatNumber: rtcChannelDetail.value!.anchorInfo!.seatNumber,
isFriend: rtcChannelDetail.value!.anchorInfo!.isFriend,
isMicrophoneOn: rtcChannelDetail.value!.anchorInfo!.isMicrophoneOn,
isVideoOn: rtcChannelDetail.value!.anchorInfo!.isVideoOn,
uid: message['uid'] is int
? message['uid'] as int
: int.tryParse(message['uid']?.toString() ?? ''),
);
final newDetail = RtcChannelDetail(
channelId: currentDetail.channelId,
anchorInfo: currentDetail.anchorInfo,
maleInfo: currentDetail.maleInfo,
femaleInfo: femaleInfo,
);
rtcChannelDetail.value = newDetail;
}
// }
}else if (message['type'] == 'leave_chat') {
if (message['role'] == 'male_audience') {
final newDetail = RtcChannelDetail(
channelId: rtcChannelDetail.value!.channelId,
anchorInfo: rtcChannelDetail.value!.anchorInfo,
maleInfo: null,
femaleInfo: rtcChannelDetail.value!.femaleInfo,
);
rtcChannelDetail.value = newDetail;
} else if (message['role'] == 'female_audience') {
final newDetail = RtcChannelDetail(
channelId: rtcChannelDetail.value!.channelId,
anchorInfo: rtcChannelDetail.value!.anchorInfo,
maleInfo: rtcChannelDetail.value!.maleInfo?.uid != message['uid'] ? rtcChannelDetail.value!.maleInfo : null,
femaleInfo: rtcChannelDetail.value!.femaleInfo?.uid != message['uid'] ? rtcChannelDetail.value!.femaleInfo : null,
);
rtcChannelDetail.value = newDetail;
}
}
}
} }

4
lib/network/rtc_api.dart

@ -40,9 +40,9 @@ abstract class RtcApi {
/// RTC /// RTC
@GET(ApiUrls.getDatingRtcChannelUserDetail) @GET(ApiUrls.getDatingRtcChannelUserDetail)
Future<HttpResponse<BaseResponse<dynamic>>> getDatingRtcChannelUserDetail(
Future<HttpResponse<BaseResponse<RtcSeatUserInfo>>> getDatingRtcChannelUserDetail(
@Query('channelId') String channelId, @Query('channelId') String channelId,
@Query('uId') String uId,
@Query('uId') int uId,
); );
/// / RTC /// / RTC

14
lib/network/rtc_api.g.dart

@ -185,10 +185,8 @@ class _RtcApi implements RtcApi {
} }
@override @override
Future<HttpResponse<BaseResponse<dynamic>>> getDatingRtcChannelUserDetail(
String channelId,
String uId,
) async {
Future<HttpResponse<BaseResponse<RtcSeatUserInfo>>>
getDatingRtcChannelUserDetail(String channelId, int uId) async {
final _extra = <String, dynamic>{}; final _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{ final queryParameters = <String, dynamic>{
r'channelId': channelId, r'channelId': channelId,
@ -196,7 +194,7 @@ class _RtcApi implements RtcApi {
}; };
final _headers = <String, dynamic>{}; final _headers = <String, dynamic>{};
const Map<String, dynamic>? _data = null; const Map<String, dynamic>? _data = null;
final _options = _setStreamType<HttpResponse<BaseResponse<dynamic>>>(
final _options = _setStreamType<HttpResponse<BaseResponse<RtcSeatUserInfo>>>(
Options(method: 'GET', headers: _headers, extra: _extra) Options(method: 'GET', headers: _headers, extra: _extra)
.compose( .compose(
_dio.options, _dio.options,
@ -207,11 +205,11 @@ class _RtcApi implements RtcApi {
.copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)),
); );
final _result = await _dio.fetch<Map<String, dynamic>>(_options); final _result = await _dio.fetch<Map<String, dynamic>>(_options);
late BaseResponse<dynamic> _value;
late BaseResponse<RtcSeatUserInfo> _value;
try { try {
_value = BaseResponse<dynamic>.fromJson(
_value = BaseResponse<RtcSeatUserInfo>.fromJson(
_result.data!, _result.data!,
(json) => json as dynamic,
(json) => RtcSeatUserInfo.fromJson(json as Map<String, dynamic>),
); );
} on Object catch (e, s) { } on Object catch (e, s) {
errorLogger?.logError(e, s, _options); errorLogger?.logError(e, s, _options);

18
lib/rtc/rtc_manager.dart

@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../controller/discover/room_controller.dart'; import '../controller/discover/room_controller.dart';
import '../network/network_service.dart';
import '../pages/discover/live_room_page.dart'; import '../pages/discover/live_room_page.dart';
/// RTC /// RTC
@ -15,6 +16,7 @@ class RTCManager {
final ValueNotifier<List<int>> remoteUsersNotifier = ValueNotifier<List<int>>( final ValueNotifier<List<int>> remoteUsersNotifier = ValueNotifier<List<int>>(
<int>[], <int>[],
); );
NetworkService get _networkService => NetworkService();
RtcEngine? get engine => _engine; RtcEngine? get engine => _engine;
bool get isInChannel => _isInChannel; bool get isInChannel => _isInChannel;
int? get currentUid => _currentUid; int? get currentUid => _currentUid;
@ -119,7 +121,6 @@ class RTCManager {
_currentChannelId = connection.channelId; _currentChannelId = connection.channelId;
print('加入频道成功,频道名:${connection.channelId},耗时:${elapsed}ms'); print('加入频道成功,频道名:${connection.channelId},耗时:${elapsed}ms');
if (connection.localUid == _currentUid) { if (connection.localUid == _currentUid) {
await RTMManager.instance.subscribe(_currentChannelId ?? ''); await RTMManager.instance.subscribe(_currentChannelId ?? '');
await RTMManager.instance.publishChannelMessage( await RTMManager.instance.publishChannelMessage(
channelName: _currentChannelId ?? '', channelName: _currentChannelId ?? '',
@ -131,7 +132,7 @@ class RTCManager {
onJoinChannelSuccess!(connection, elapsed); onJoinChannelSuccess!(connection, elapsed);
} }
}, },
onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) {
onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) async{
print('用户加入,UID:$remoteUid'); print('用户加入,UID:$remoteUid');
_handleRemoteUserJoined(remoteUid); _handleRemoteUserJoined(remoteUid);
if (onUserJoined != null) { if (onUserJoined != null) {
@ -483,7 +484,7 @@ class RTCManager {
} }
/// ///
Future<void> publishVideo(CurrentRole role) async {
Future<void> publishVideo(CurrentRole role) async {
await _engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster); await _engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
await _engine?.muteLocalAudioStream(false); await _engine?.muteLocalAudioStream(false);
await _engine?.muteLocalVideoStream(false); await _engine?.muteLocalVideoStream(false);
@ -492,7 +493,9 @@ class RTCManager {
message: json.encode({ message: json.encode({
'type': 'join_chat', 'type': 'join_chat',
'uid': _currentUid, 'uid': _currentUid,
'role': role == CurrentRole.maleAudience ? 'male_audience' : 'female_audience'
'role': role == CurrentRole.maleAudience
? 'male_audience'
: 'female_audience',
}), }),
); );
} }
@ -507,7 +510,7 @@ class RTCManager {
message: json.encode({ message: json.encode({
'type': 'join_chat', 'type': 'join_chat',
'uid': _currentUid, 'uid': _currentUid,
'role': 'audience'
'role': 'audience',
}), }),
); );
} }
@ -519,10 +522,7 @@ class RTCManager {
await _engine?.muteLocalVideoStream(true); await _engine?.muteLocalVideoStream(true);
await RTMManager.instance.publishChannelMessage( await RTMManager.instance.publishChannelMessage(
channelName: _currentChannelId ?? '', channelName: _currentChannelId ?? '',
message: json.encode({
'type': 'leave_chat',
'uid': _currentUid,
}),
message: json.encode({'type': 'leave_chat', 'uid': _currentUid}),
); );
} }
} }

9
lib/service/live_chat_message_service.dart

@ -1,11 +1,13 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:agora_rtm/agora_rtm.dart'; import 'package:agora_rtm/agora_rtm.dart';
import 'package:dating_touchme_app/controller/discover/room_controller.dart';
import 'package:dating_touchme_app/controller/global.dart'; import 'package:dating_touchme_app/controller/global.dart';
import 'package:dating_touchme_app/model/live/live_chat_message.dart'; import 'package:dating_touchme_app/model/live/live_chat_message.dart';
import 'package:dating_touchme_app/rtc/rtc_manager.dart'; import 'package:dating_touchme_app/rtc/rtc_manager.dart';
import 'package:dating_touchme_app/rtc/rtm_manager.dart'; import 'package:dating_touchme_app/rtc/rtm_manager.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
/// ///
@ -56,7 +58,7 @@ class LiveChatMessageService {
} }
/// ///
void _handleIncomingMessage(MessageEvent event) {
void _handleIncomingMessage(MessageEvent event) async{
try { try {
// //
final messageText = _parseMessageContent(event.message); final messageText = _parseMessageContent(event.message);
@ -66,8 +68,9 @@ class LiveChatMessageService {
if (messageData['type'] == 'chat_message') { if (messageData['type'] == 'chat_message') {
final chatMessage = LiveChatMessage.fromJson(messageData); final chatMessage = LiveChatMessage.fromJson(messageData);
onMessageReceived?.call(chatMessage); onMessageReceived?.call(chatMessage);
}else if(messageData['type'] == 'like_message'){
}else{
RoomController controller = Get.find<RoomController>();
await controller.receiveRTCMessage(messageData);
} }
} catch (e, stackTrace) { } catch (e, stackTrace) {
final error = '解析RTM消息失败: $e'; final error = '解析RTM消息失败: $e';

6
lib/widget/live/live_room_anchor_showcase.dart

@ -100,6 +100,8 @@ class _LiveRoomAnchorShowcaseState extends State<LiveRoomAnchorShowcase> {
), ),
SizedBox(height: 5.w), SizedBox(height: 5.w),
Obx(() { Obx(() {
// 访
_roomController.rtcChannelDetail.value;
final rtcChannelDetail = final rtcChannelDetail =
_roomController.rtcChannelDetail.value; _roomController.rtcChannelDetail.value;
return Row( return Row(
@ -188,7 +190,9 @@ class _LiveRoomAnchorShowcaseState extends State<LiveRoomAnchorShowcase> {
final joined = _rtcManager.channelJoinedNotifier.value; final joined = _rtcManager.channelJoinedNotifier.value;
// //
final bool isCurrentUser = _roomController.currentRole == CurrentRole.maleAudience || _roomController.currentRole == CurrentRole.femaleAudience;
final bool isCurrentUser =
_roomController.currentRole == CurrentRole.maleAudience ||
_roomController.currentRole == CurrentRole.femaleAudience;
return Stack( return Stack(
children: [ children: [

12
lib/widget/live/live_room_notice_chat_panel.dart

@ -76,10 +76,6 @@ class _LiveRoomNoticeChatPanelState extends State<LiveRoomNoticeChatPanel> {
), ),
SizedBox(width: 18.w), SizedBox(width: 18.w),
Obx((){ Obx((){
Get.log("${controller.rtcChannelDetail.value?.maleInfo}");
Get.log("${controller.rtcChannelDetail.value?.femaleInfo}");
Get.log("${controller.currentRole}");
Get.log("${GlobalData().userData?.genderCode}");
if(controller.rtcChannelDetail.value?.maleInfo == null && GlobalData().userData?.genderCode == 0 && controller.currentRole != CurrentRole.broadcaster || if(controller.rtcChannelDetail.value?.maleInfo == null && GlobalData().userData?.genderCode == 0 && controller.currentRole != CurrentRole.broadcaster ||
controller.rtcChannelDetail.value?.femaleInfo == null && GlobalData().userData?.genderCode == 1 && controller.currentRole != CurrentRole.broadcaster){ controller.rtcChannelDetail.value?.femaleInfo == null && GlobalData().userData?.genderCode == 1 && controller.currentRole != CurrentRole.broadcaster){
return Container( return Container(
@ -88,8 +84,8 @@ class _LiveRoomNoticeChatPanelState extends State<LiveRoomNoticeChatPanel> {
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 10.w), padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 10.w),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.w), borderRadius: BorderRadius.circular(10.w),
gradient: const LinearGradient(
colors: [Color(0xFF7C63FF), Color(0xFF987CFF)],
gradient: LinearGradient(
colors: controller.isLive ? [Colors.grey] : [Color(0xFF7C63FF), Color(0xFF987CFF)],
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
), ),
@ -106,7 +102,7 @@ class _LiveRoomNoticeChatPanelState extends State<LiveRoomNoticeChatPanel> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text(
'免费连麦',
controller.isLive ? '申请连麦中' : '免费连麦',
style: TextStyle( style: TextStyle(
fontSize: 13.w, fontSize: 13.w,
color: Colors.white, color: Colors.white,
@ -114,7 +110,7 @@ class _LiveRoomNoticeChatPanelState extends State<LiveRoomNoticeChatPanel> {
), ),
), ),
SizedBox(height: 2.w), SizedBox(height: 2.w),
Text(
controller.isLive ? const SizedBox() :Text(
'剩余2张相亲卡', '剩余2张相亲卡',
style: TextStyle( style: TextStyle(
fontSize: 9.w, fontSize: 9.w,

Loading…
Cancel
Save