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: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_detail.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: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, //
}
@ -26,6 +27,7 @@ class RoomController extends GetxController {
final NetworkService _networkService;
CurrentRole currentRole = CurrentRole.normalUser;
bool isLive = false;
///
final Rxn<RtcChannelData> rtcChannel = Rxn<RtcChannelData>();
final Rxn<RtcChannelDetail> rtcChannelDetail = Rxn<RtcChannelDetail>();
@ -158,7 +160,10 @@ class RoomController extends GetxController {
'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,
'isVideoOn':
role == CurrentRole.maleAudience || role == CurrentRole.femaleAudience
? true
: false,
};
final response = await _networkService.rtcApi.connectRtcChannel(data);
if (!response.data.isSuccess) {
@ -166,21 +171,38 @@ class RoomController extends GetxController {
return;
}
currentRole = role;
if(role == CurrentRole.maleAudience || role == CurrentRole.femaleAudience){
if (role == CurrentRole.maleAudience ||
role == CurrentRole.femaleAudience) {
await RTCManager.instance.publishVideo(role);
}else{
} else {
await RTCManager.instance.publishAudio();
}
isLive = true;
}
Future<void> leaveChat() async {
final data = {
'channelId': rtcChannel.value?.channelId
};
final data = {'channelId': rtcChannel.value?.channelId};
final response = await _networkService.rtcApi.disconnectRtcChannel(data);
if(response.data.isSuccess){
if (response.data.isSuccess) {
isLive = false;
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 {
isLive = false;
currentRole = CurrentRole.normalUser;;
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
@GET(ApiUrls.getDatingRtcChannelUserDetail)
Future<HttpResponse<BaseResponse<dynamic>>> getDatingRtcChannelUserDetail(
Future<HttpResponse<BaseResponse<RtcSeatUserInfo>>> getDatingRtcChannelUserDetail(
@Query('channelId') String channelId,
@Query('uId') String uId,
@Query('uId') int uId,
);
/// / RTC

14
lib/network/rtc_api.g.dart

@ -185,10 +185,8 @@ class _RtcApi implements RtcApi {
}
@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 queryParameters = <String, dynamic>{
r'channelId': channelId,
@ -196,7 +194,7 @@ class _RtcApi implements RtcApi {
};
final _headers = <String, dynamic>{};
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)
.compose(
_dio.options,
@ -207,11 +205,11 @@ class _RtcApi implements RtcApi {
.copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)),
);
final _result = await _dio.fetch<Map<String, dynamic>>(_options);
late BaseResponse<dynamic> _value;
late BaseResponse<RtcSeatUserInfo> _value;
try {
_value = BaseResponse<dynamic>.fromJson(
_value = BaseResponse<RtcSeatUserInfo>.fromJson(
_result.data!,
(json) => json as dynamic,
(json) => RtcSeatUserInfo.fromJson(json as Map<String, dynamic>),
);
} on Object catch (e, s) {
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 '../controller/discover/room_controller.dart';
import '../network/network_service.dart';
import '../pages/discover/live_room_page.dart';
/// RTC
@ -15,6 +16,7 @@ class RTCManager {
final ValueNotifier<List<int>> remoteUsersNotifier = ValueNotifier<List<int>>(
<int>[],
);
NetworkService get _networkService => NetworkService();
RtcEngine? get engine => _engine;
bool get isInChannel => _isInChannel;
int? get currentUid => _currentUid;
@ -119,7 +121,6 @@ class RTCManager {
_currentChannelId = connection.channelId;
print('加入频道成功,频道名:${connection.channelId},耗时:${elapsed}ms');
if (connection.localUid == _currentUid) {
await RTMManager.instance.subscribe(_currentChannelId ?? '');
await RTMManager.instance.publishChannelMessage(
channelName: _currentChannelId ?? '',
@ -131,7 +132,7 @@ class RTCManager {
onJoinChannelSuccess!(connection, elapsed);
}
},
onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) {
onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) async{
print('用户加入,UID:$remoteUid');
_handleRemoteUserJoined(remoteUid);
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?.muteLocalAudioStream(false);
await _engine?.muteLocalVideoStream(false);
@ -492,7 +493,9 @@ class RTCManager {
message: json.encode({
'type': 'join_chat',
'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({
'type': 'join_chat',
'uid': _currentUid,
'role': 'audience'
'role': 'audience',
}),
);
}
@ -519,10 +522,7 @@ class RTCManager {
await _engine?.muteLocalVideoStream(true);
await RTMManager.instance.publishChannelMessage(
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:typed_data';
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/model/live/live_chat_message.dart';
import 'package:dating_touchme_app/rtc/rtc_manager.dart';
import 'package:dating_touchme_app/rtc/rtm_manager.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
///
@ -56,7 +58,7 @@ class LiveChatMessageService {
}
///
void _handleIncomingMessage(MessageEvent event) {
void _handleIncomingMessage(MessageEvent event) async{
try {
//
final messageText = _parseMessageContent(event.message);
@ -66,8 +68,9 @@ class LiveChatMessageService {
if (messageData['type'] == 'chat_message') {
final chatMessage = LiveChatMessage.fromJson(messageData);
onMessageReceived?.call(chatMessage);
}else if(messageData['type'] == 'like_message'){
}else{
RoomController controller = Get.find<RoomController>();
await controller.receiveRTCMessage(messageData);
}
} catch (e, stackTrace) {
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),
Obx(() {
// 访
_roomController.rtcChannelDetail.value;
final rtcChannelDetail =
_roomController.rtcChannelDetail.value;
return Row(
@ -188,7 +190,9 @@ class _LiveRoomAnchorShowcaseState extends State<LiveRoomAnchorShowcase> {
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(
children: [

12
lib/widget/live/live_room_notice_chat_panel.dart

@ -76,10 +76,6 @@ class _LiveRoomNoticeChatPanelState extends State<LiveRoomNoticeChatPanel> {
),
SizedBox(width: 18.w),
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 ||
controller.rtcChannelDetail.value?.femaleInfo == null && GlobalData().userData?.genderCode == 1 && controller.currentRole != CurrentRole.broadcaster){
return Container(
@ -88,8 +84,8 @@ class _LiveRoomNoticeChatPanelState extends State<LiveRoomNoticeChatPanel> {
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 10.w),
decoration: BoxDecoration(
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,
end: Alignment.bottomRight,
),
@ -106,7 +102,7 @@ class _LiveRoomNoticeChatPanelState extends State<LiveRoomNoticeChatPanel> {
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'免费连麦',
controller.isLive ? '申请连麦中' : '免费连麦',
style: TextStyle(
fontSize: 13.w,
color: Colors.white,
@ -114,7 +110,7 @@ class _LiveRoomNoticeChatPanelState extends State<LiveRoomNoticeChatPanel> {
),
),
SizedBox(height: 2.w),
Text(
controller.isLive ? const SizedBox() :Text(
'剩余2张相亲卡',
style: TextStyle(
fontSize: 9.w,

Loading…
Cancel
Save