import 'dart:async'; import 'package:agora_rtc_engine/agora_rtc_engine.dart'; import 'package:fluro/fluro.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:yumi/app_localizations.dart'; import 'package:yumi/app/constants/sc_room_msg_type.dart'; import 'package:yumi/shared/tools/sc_permission_utils.dart'; import 'package:yumi/shared/tools/sc_room_utils.dart'; import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; import 'package:yumi/services/room/rc_room_manager.dart'; import 'package:yumi/services/audio/rtm_manager.dart'; import 'package:yumi/services/auth/user_profile_manager.dart'; import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; import 'package:provider/provider.dart'; import 'package:yumi/ui_kit/components/sc_tts.dart'; import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/app/routes/sc_routes.dart'; import 'package:yumi/app/routes/sc_fluro_navigator.dart'; import 'package:yumi/shared/tools/sc_lk_dialog_util.dart'; import 'package:yumi/shared/tools/sc_loading_manager.dart'; import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; import 'package:yumi/shared/business_logic/models/res/join_room_res.dart'; import 'package:yumi/shared/business_logic/models/res/login_res.dart'; import 'package:yumi/shared/business_logic/models/res/mic_res.dart'; import 'package:yumi/shared/business_logic/models/res/room_res.dart'; import 'package:yumi/modules/room/voice_room_route.dart'; import 'package:yumi/ui_kit/widgets/room/empty_mai_select.dart'; import 'package:yumi/ui_kit/widgets/room/room_user_info_card.dart'; import '../../shared/tools/sc_heartbeat_utils.dart'; import '../../shared/data_sources/models/enum/sc_heartbeat_status.dart'; import '../../shared/data_sources/models/enum/sc_room_info_event_type.dart'; import '../../shared/data_sources/models/enum/sc_room_roles_type.dart'; import '../../shared/business_logic/models/res/sc_is_follow_room_res.dart'; import '../../shared/business_logic/models/res/sc_room_red_packet_list_res.dart'; import '../../shared/business_logic/models/res/sc_room_rocket_status_res.dart'; import '../../shared/business_logic/models/res/sc_room_theme_list_res.dart'; import '../../shared/tools/sc_room_profile_cache.dart'; import '../../ui_kit/components/sc_float_ichart.dart'; typedef OnSoundVoiceChange = Function(num index, int volum); typedef RtcProvider = RealTimeCommunicationManager; class RealTimeCommunicationManager extends ChangeNotifier { static const Duration _micListPollingInterval = Duration(seconds: 2); static const Duration _onlineUsersPollingInterval = Duration(seconds: 3); bool needUpDataUserInfo = false; bool _roomVisualEffectsEnabled = false; bool _isExitingCurrentVoiceRoomSession = false; Timer? _micListPollingTimer; Timer? _onlineUsersPollingTimer; bool _isRefreshingMicList = false; bool _isRefreshingOnlineUsers = false; ///当前所在房间 JoinRoomRes? currenRoom; /// 声音音量变化监听 final List _onSoundVoiceChangeList = []; ///麦位 Map roomWheatMap = {}; RtcEngine? engine; BuildContext? context; RtmProvider? rtmProvider; SCIsFollowRoomRes? isFollowRoomRes; SCRoomRocketStatusRes? roomRocketStatus; ///房间是否静音 bool roomIsMute = false; bool closeFullGame = false; ///禁音开关 默认关闭 bool isMic = true; ///在线用户列表 List onlineUsers = []; ///房间管理员 List managerUsers = []; ///音乐是否正在播放 bool isMusicPlaying = false; ///房间红包列表 List redPacketList = []; num roomTaskClaimableCount = 0; initializeRealTimeCommunicationManager(BuildContext context) { this.context = context; } bool get roomVisualEffectsEnabled => _roomVisualEffectsEnabled; bool get isExitingCurrentVoiceRoomSession => _isExitingCurrentVoiceRoomSession; bool get shouldShowRoomVisualEffects => currenRoom != null && _roomVisualEffectsEnabled; void setRoomVisualEffectsEnabled(bool enabled) { if (_roomVisualEffectsEnabled == enabled) { return; } _roomVisualEffectsEnabled = enabled; notifyListeners(); } void _setExitingCurrentVoiceRoomSession(bool enabled, {bool notify = true}) { if (_isExitingCurrentVoiceRoomSession == enabled) { return; } _isExitingCurrentVoiceRoomSession = enabled; if (notify) { notifyListeners(); } } void _startRoomStatePolling() { _stopRoomStatePolling(); if ((currenRoom?.roomProfile?.roomProfile?.id ?? "").isEmpty) { return; } _micListPollingTimer = Timer.periodic(_micListPollingInterval, (_) { retrieveMicrophoneList(notifyIfUnchanged: false).catchError((_) {}); }); _onlineUsersPollingTimer = Timer.periodic(_onlineUsersPollingInterval, (_) { fetchOnlineUsersList(notifyIfUnchanged: false).catchError((_) {}); }); } void _stopRoomStatePolling() { _micListPollingTimer?.cancel(); _onlineUsersPollingTimer?.cancel(); _micListPollingTimer = null; _onlineUsersPollingTimer = null; _isRefreshingMicList = false; _isRefreshingOnlineUsers = false; } bool _sameOnlineUsers( List previous, List next, ) { if (previous.length != next.length) { return false; } for (int i = 0; i < previous.length; i++) { if (!_sameUserProfile(previous[i], next[i])) { return false; } } return true; } bool _sameUserProfile( SocialChatUserProfile? previous, SocialChatUserProfile? next, ) { if (identical(previous, next)) { return true; } if (previous == null || next == null) { return previous == next; } return previous.id == next.id && previous.account == next.account && previous.userAvatar == next.userAvatar && previous.userNickname == next.userNickname && previous.userSex == next.userSex && previous.roles == next.roles && previous.heartbeatVal == next.heartbeatVal; } bool _sameMicMaps(Map previous, Map next) { if (previous.length != next.length) { return false; } for (final entry in next.entries) { if (!_sameMic(previous[entry.key], entry.value)) { return false; } } return true; } bool _sameMic(MicRes? previous, MicRes? next) { if (identical(previous, next)) { return true; } if (previous == null || next == null) { return previous == next; } return previous.micIndex == next.micIndex && previous.micLock == next.micLock && previous.micMute == next.micMute && _sameUserProfile(previous.user, next.user); } void _refreshManagerUsers(List users) { managerUsers ..clear() ..addAll( users.where((user) => user.roles == SCRoomRolesType.ADMIN.name), ); } Map _buildMicMap(List roomWheatList) { final previousMap = roomWheatMap; final nextMap = {}; for (final roomWheat in roomWheatList) { final micIndex = roomWheat.micIndex; if (micIndex == null) { continue; } final previousMic = previousMap[micIndex]; final shouldPreserveTransientState = previousMic?.user?.id == roomWheat.user?.id && previousMic?.user?.id != null; nextMap[micIndex] = shouldPreserveTransientState ? roomWheat.copyWith( emojiPath: (roomWheat.emojiPath ?? "").isNotEmpty ? roomWheat.emojiPath : previousMic?.emojiPath, type: (roomWheat.type ?? "").isNotEmpty ? roomWheat.type : previousMic?.type, number: (roomWheat.number ?? "").isNotEmpty ? roomWheat.number : previousMic?.number, ) : roomWheat; } return nextMap; } void _syncSelfMicRuntimeState() { final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id; if ((currentUserId ?? "").isEmpty) { return; } MicRes? currentUserMic; for (final mic in roomWheatMap.values) { if (mic.user?.id == currentUserId) { currentUserMic = mic; break; } } if (currentUserMic == null) { SCHeartbeatUtils.cancelAnchorTimer(); engine?.setClientRole(role: ClientRoleType.clientRoleAudience); engine?.muteLocalAudioStream(true); return; } SCHeartbeatUtils.scheduleAnchorHeartbeat( currenRoom?.roomProfile?.roomProfile?.id ?? "", ); if ((currentUserMic.micMute ?? false) || isMic) { engine?.setClientRole(role: ClientRoleType.clientRoleAudience); engine?.muteLocalAudioStream(true); return; } adjustRecordingSignalVolume(100); engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster); engine?.muteLocalAudioStream(false); } void _applyMicSnapshot( Map nextMap, { bool notifyIfUnchanged = true, }) { final changed = !_sameMicMaps(roomWheatMap, nextMap); roomWheatMap = nextMap; _syncSelfMicRuntimeState(); if (changed || notifyIfUnchanged) { notifyListeners(); } } Future joinAgoraVoiceChannel() async { try { engine = await _initAgoraRtcEngine(); engine?.setAudioProfile( profile: AudioProfileType.audioProfileSpeechStandard, scenario: AudioScenarioType.audioScenarioGameStreaming, ); engine?.enableAudioVolumeIndication( interval: 500, smooth: 3, reportVad: true, ); await engine?.disableVideo(); await engine?.setLocalPublishFallbackOption( StreamFallbackOptions.streamFallbackOptionAudioOnly, ); engine?.registerEventHandler( RtcEngineEventHandler( onError: (ErrorCodeType err, String msg) { print('rtc错误${err}'); }, onAudioMixingStateChanged: ( AudioMixingStateType state, AudioMixingReasonType reason, ) { switch (state) { case AudioMixingStateType.audioMixingStatePlaying: isMusicPlaying = true; break; case AudioMixingStateType.audioMixingStateStopped: case AudioMixingStateType.audioMixingStateFailed: isMusicPlaying = false; break; default: break; } }, onJoinChannelSuccess: (RtcConnection connection, int elapsed) { print('rtc 自己加入 ${connection.channelId} ${connection.localUid}'); }, onUserJoined: (connection, remoteUid, elapsed) { print('rtc用户 $remoteUid 加入了频道'); }, // 监听远端用户离开 onUserOffline: (connection, remoteUid, reason) { print('rtc用户 $remoteUid 离开了频道 (原因: ${reason})'); }, onTokenPrivilegeWillExpire: ( RtcConnection connection, String token, ) async { var rtcToken = await SCAccountRepository().getRtcToken( currenRoom?.roomProfile?.roomProfile?.id ?? "", AccountStorage().getCurrentUser()?.userProfile?.id ?? "", isPublisher: isOnMai(), ); engine?.renewToken(rtcToken.rtcToken ?? ""); }, onAudioVolumeIndication: initializeAudioVolumeIndicationCallback, ), ); var rtcToken = await SCAccountRepository().getRtcToken( currenRoom?.roomProfile?.roomProfile?.id ?? "", AccountStorage().getCurrentUser()?.userProfile?.id ?? "", isPublisher: false, ); await engine?.joinChannel( token: rtcToken.rtcToken ?? "", channelId: currenRoom?.roomProfile?.roomProfile?.id ?? "", uid: int.parse( AccountStorage().getCurrentUser()?.userProfile?.account ?? "0", ), options: ChannelMediaOptions( // 自动订阅所有视频流 autoSubscribeVideo: false, // 自动订阅所有音频流 autoSubscribeAudio: true, // 发布摄像头采集的视频 publishCameraTrack: false, // 发布麦克风采集的音频 publishMicrophoneTrack: true, // 设置用户角色为 clientRoleBroadcaster(主播)或 clientRoleAudience(观众) clientRoleType: ClientRoleType.clientRoleAudience, channelProfile: ChannelProfileType.channelProfileLiveBroadcasting, ), ); engine?.muteAllRemoteAudioStreams(roomIsMute); } catch (e) { SCTts.show("Join room fail"); exitCurrentVoiceRoomSession(false); print('加入失败:${e.runtimeType},${e.toString()}'); } } Future _initAgoraRtcEngine() async { RtcEngine? engine; while (engine == null) { engine = createAgoraRtcEngine(); await engine.initialize( RtcEngineContext(appId: SCGlobalConfig.agoraRtcAppid), ); } return engine; } void initializeAudioVolumeIndicationCallback( RtcConnection connection, List speakers, int speakerNumber, int totalVolume, ) { // if (user == null) { // throw "本地User尚未初始化"; // } // print('初始化声音监听器:${user.toJson()}'); ///声音全部清零 roomWheatMap.forEach((k, v) { roomWheatMap[k]!.setVolume = 0; }); for (var info in speakers) { num? uid = info.uid; int voloum = info.volume!; print('${info.uid}在说话---${voloum}'); if (voloum > 0) { ///是本地声音 if (uid == 0) { uid = num.parse( AccountStorage().getCurrentUser()!.userProfile!.account!, ); } ///在哪个麦 roomWheatMap.forEach((k, v) { if (v.user != null) { if (num.parse(v.user!.account!) == uid) { roomWheatMap[k]!.setVolume = voloum; for (var element in _onSoundVoiceChangeList) { element(k, voloum); } } } }); } } } ///关注/取消房间 void followCurrentVoiceRoom() async { var result = await SCAccountRepository().followRoom( currenRoom?.roomProfile?.roomProfile?.id ?? "", ); if (isFollowRoomRes != null) { isFollowRoomRes = isFollowRoomRes!.copyWith( followRoom: !isFollowRoomRes!.followRoom!, ); } else { isFollowRoomRes = SCIsFollowRoomRes(followRoom: result); } SmartDialog.dismiss(tag: "unFollowDialog"); notifyListeners(); } int startTime = 0; String? _preferNonEmpty(String? primary, String? fallback) { if ((primary ?? "").trim().isNotEmpty) { return primary; } if ((fallback ?? "").trim().isNotEmpty) { return fallback; } return primary ?? fallback; } void updateCurrentRoomBasicInfo({ String? roomCover, String? roomName, String? roomDesc, }) { final currentRoomProfile = currenRoom?.roomProfile?.roomProfile; if (currenRoom == null || currentRoomProfile == null) { return; } currenRoom = currenRoom?.copyWith( roomProfile: currenRoom?.roomProfile?.copyWith( roomProfile: currentRoomProfile.copyWith( roomCover: _preferNonEmpty(roomCover, currentRoomProfile.roomCover), roomName: _preferNonEmpty(roomName, currentRoomProfile.roomName), roomDesc: _preferNonEmpty(roomDesc, currentRoomProfile.roomDesc), ), ), ); SCRoomProfileCache.saveRoomProfile( roomId: currentRoomProfile.id ?? "", roomCover: roomCover, roomName: roomName, roomDesc: roomDesc, ); notifyListeners(); } void joinVoiceRoomSession( BuildContext context, String roomId, { String? pwd, bool clearRoomData = false, bool needOpenRedenvelope = false, String redPackId = "", }) async { int nextTime = DateTime.now().millisecondsSinceEpoch; if (nextTime - startTime < 1000) { //频繁点击 return; } startTime = nextTime; if (clearRoomData) { _clearData(); } bool hasPermission = await SCPermissionUtils.checkMicrophonePermission(); if (!hasPermission) { SCTts.show('Microphone permission is denied.'); throw ArgumentError('Microphone permission is denied.'); } if (roomId == currenRoom?.roomProfile?.roomProfile?.id) { ///最小化进入房间,或者进入的是同一个房间 final loaded = await loadRoomInfo( currenRoom?.roomProfile?.roomProfile?.id ?? "", ); if (!loaded) { return; } if (!context.mounted) { return; } Provider.of( context, listen: false, ).fetchUserProfileData(); retrieveMicrophoneList(); fetchOnlineUsersList(); _startRoomStatePolling(); setRoomVisualEffectsEnabled(true); SCFloatIchart().remove(); SCNavigatorUtils.push( context, '${VoiceRoomRoute.voiceRoom}?id=$roomId', transition: TransitionType.custom, transitionDuration: kOpenRoomTransitionDuration, transitionBuilder: ( BuildContext context, Animation animation, Animation secondaryAnimation, Widget child, ) { return kOpenRoomTransitionBuilder( context, animation, secondaryAnimation, child, ); }, ); } else { SCFloatIchart().remove(); if (currenRoom != null) { await exitCurrentVoiceRoomSession(false); } if (!context.mounted) { return; } rtmProvider = Provider.of(context, listen: false); SCLoadingManager.show(context: context); try { currenRoom = await SCAccountRepository().entryRoom( roomId, pwd: pwd, redPackId: redPackId, needOpenRedenvelope: needOpenRedenvelope, ); await initializeRoomSession( needOpenRedenvelope: needOpenRedenvelope, redPackId: redPackId, ); notifyListeners(); } catch (e) { await resetLocalRoomState(fallbackRtmProvider: rtmProvider); rethrow; } finally { SCLoadingManager.hide(); } } } ///初始化房间相关 Future initializeRoomSession({ bool needOpenRedenvelope = false, String? redPackId, }) async { if ((currenRoom?.roomProfile?.roomProfile?.event == SCRoomInfoEventType.WAITING_CONFIRMED.name || currenRoom?.roomProfile?.roomProfile?.event == SCRoomInfoEventType.ID_CHANGE.name) && currenRoom?.entrants?.roles == SCRoomRolesType.HOMEOWNER.name) { ///需要去修改房间信息,创建群聊 SCNavigatorUtils.push( context!, "${VoiceRoomRoute.roomEdit}?need=true", replace: false, ); return Future; } SCRoomUtils.roomSCGlobalConfig( currenRoom?.roomProfile?.roomProfile?.id ?? "", ); if (currenRoom?.roomProfile?.roomProfile?.id != AccountStorage().getCurrentUser()?.userProfile?.id) { SCChatRoomRepository() .isFollowRoom(currenRoom?.roomProfile?.roomProfile?.id ?? "") .then((value) { isFollowRoomRes = value; notifyListeners(); }); } setRoomVisualEffectsEnabled(true); SCNavigatorUtils.push(context!, VoiceRoomRoute.voiceRoom, replace: false); var joinResult = await rtmProvider?.joinRoomGroup( currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "", "", ); SCChatRoomRepository() .rocketClaim(currenRoom?.roomProfile?.roomProfile?.id ?? "") .catchError((e) { return true; // 错误已处理 }); rtmProvider?.addMsg( Msg( groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "", msg: SCAppLocalizations.of(context!)!.systemRoomTips, type: SCRoomMsgType.systemTips, ), ); rtmProvider?.addMsg( Msg( groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "", msg: currenRoom?.roomProfile?.roomProfile?.roomDesc, type: SCRoomMsgType.systemTips, ), ); ///获取麦位 retrieveMicrophoneList(); fetchOnlineUsersList(); _startRoomStatePolling(); Provider.of( context!, listen: false, ).fetchContributionLevelData( currenRoom?.roomProfile?.roomProfile?.id ?? "", ); await joinAgoraVoiceChannel(); if (joinResult?.code == 0) { Msg joinMsg = Msg( groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "", user: AccountStorage().getCurrentUser()?.userProfile, msg: "", role: currenRoom?.entrants?.roles ?? "", type: SCRoomMsgType.joinRoom, ); rtmProvider?.dispatchMessage(joinMsg, addLocal: true); if (SCGlobalConfig.isEntryVehicleAnimation) { if (AccountStorage().getCurrentUser()?.userProfile?.getMountains() != null && shouldShowRoomVisualEffects) { Future.delayed(Duration(milliseconds: 550), () { SCGiftVapSvgaManager().play( AccountStorage() .getCurrentUser() ?.userProfile ?.getMountains() ?.sourceUrl ?? "", priority: 100, type: 1, ); }); } } if (rtmProvider?.msgUserJoinListener != null) { rtmProvider?.msgUserJoinListener!(joinMsg); } } SCChatRoomRepository() .rocketStatus(currenRoom?.roomProfile?.roomProfile?.id ?? "") .then((res) { roomRocketStatus = res; notifyListeners(); }) .catchError((e) {}); loadRoomRedPacketList(1); fetchRoomTaskClaimableCount(); } ///更新房间火箭信息 void updateRoomRocketConfigurationStatus(SCRoomRocketStatusRes res) { roomRocketStatus = res; notifyListeners(); } ///获取在线用户 Future fetchOnlineUsersList({bool notifyIfUnchanged = true}) async { final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? ""; if (roomId.isEmpty || _isRefreshingOnlineUsers) { return; } _isRefreshingOnlineUsers = true; try { final fetchedUsers = await SCChatRoomRepository().roomOnlineUsers(roomId); if (roomId != currenRoom?.roomProfile?.roomProfile?.id) { return; } final changed = !_sameOnlineUsers(onlineUsers, fetchedUsers); onlineUsers = fetchedUsers; _refreshManagerUsers(onlineUsers); if (changed || notifyIfUnchanged) { notifyListeners(); } } finally { _isRefreshingOnlineUsers = false; } } Future retrieveMicrophoneList({bool notifyIfUnchanged = true}) async { final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? ""; if (roomId.isEmpty || _isRefreshingMicList) { return; } _isRefreshingMicList = true; bool isOnMic = false; try { final roomWheatList = await SCChatRoomRepository().micList(roomId); if (roomId != currenRoom?.roomProfile?.roomProfile?.id) { return; } final nextMap = _buildMicMap(roomWheatList); for (final roomWheat in nextMap.values) { if (roomWheat.user != null && roomWheat.user!.id == AccountStorage().getCurrentUser()?.userProfile?.id) { isOnMic = true; break; } } _applyMicSnapshot(nextMap, notifyIfUnchanged: notifyIfUnchanged); SCHeartbeatUtils.scheduleHeartbeat( SCHeartbeatStatus.VOICE_LIVE.name, isOnMic, roomId: currenRoom?.roomProfile?.roomProfile?.id, ); } finally { _isRefreshingMicList = false; } } void fetchRoomTaskClaimableCount() { SCChatRoomRepository() .roomTaskClaimableCount() .then((res) { roomTaskClaimableCount = res.claimableCount ?? 0; notifyListeners(); }) .catchError((e) {}); } Future> loadRoomRedPacketList( int current, ) async { var result = await SCChatRoomRepository().roomRedPacketList( currenRoom?.roomProfile?.roomProfile?.id ?? "", current, ); if (current == 1) { redPacketList = result; notifyListeners(); } return result; } Future exitCurrentVoiceRoomSession(bool isLogout) async { _setExitingCurrentVoiceRoomSession(true); _stopRoomStatePolling(); try { rtmProvider?.msgAllListener = null; rtmProvider?.msgChatListener = null; rtmProvider?.msgGiftListener = null; SCLoadingManager.show(context: context); SCGiftVapSvgaManager().stopPlayback(); setRoomVisualEffectsEnabled(false); SCHeartbeatUtils.scheduleHeartbeat(SCHeartbeatStatus.ONLINE.name, false); SCHeartbeatUtils.cancelAnchorTimer(); rtmProvider?.cleanRoomData(); await engine?.leaveChannel(); await Future.delayed(Duration(milliseconds: 100)); await engine?.release(); await rtmProvider?.quitGroup( currenRoom!.roomProfile?.roomProfile?.roomAccount ?? "", ); await SCAccountRepository().quitRoom( currenRoom!.roomProfile?.roomProfile?.id ?? "", ); _clearData(); SCLoadingManager.hide(); if (!isLogout) { SCNavigatorUtils.popUntil(context!, ModalRoute.withName(SCRoutes.home)); } } catch (e) { print('rtc退出房间出错: $e'); _clearData(); SCLoadingManager.hide(); if (!isLogout) { SCNavigatorUtils.popUntil(context!, ModalRoute.withName(SCRoutes.home)); } } } Future resetLocalRoomState({RtmProvider? fallbackRtmProvider}) async { rtmProvider ??= fallbackRtmProvider; _stopRoomStatePolling(); try { SCHeartbeatUtils.cancelTimer(); SCHeartbeatUtils.cancelAnchorTimer(); final groupId = currenRoom?.roomProfile?.roomProfile?.roomAccount ?? ""; if (groupId.isNotEmpty) { await rtmProvider?.quitGroup(groupId); } await engine?.leaveChannel(); await Future.delayed(const Duration(milliseconds: 100)); await engine?.release(); } catch (e) { print('rtc清理本地房间状态出错: $e'); } finally { engine = null; rtmProvider?.cleanRoomData(); _clearData(); notifyListeners(); } } ///清空列表数据 void _clearData() { _stopRoomStatePolling(); roomRocketStatus = null; rtmProvider ?.onNewMessageListenerGroupMap["${currenRoom?.roomProfile?.roomProfile?.roomAccount}"] = null; roomWheatMap.clear(); onlineUsers.clear(); managerUsers.clear(); needUpDataUserInfo = false; SCRoomUtils.roomUsersMap.clear(); roomIsMute = false; rtmProvider?.roomAllMsgList.clear(); rtmProvider?.roomChatMsgList.clear(); redPacketList.clear(); currenRoom = null; _roomVisualEffectsEnabled = false; _isExitingCurrentVoiceRoomSession = false; isMic = true; isMusicPlaying = false; DataPersistence.setLastTimeRoomId(""); SCGiftVapSvgaManager().stopPlayback(); SCRoomUtils.closeAllDialogs(); SCGlobalConfig.resetVisualEffectSwitchesToRecommendedDefaults(); } void toggleRemoteAudioMuteForAllUsers() { roomIsMute = !roomIsMute; engine?.muteAllRemoteAudioStreams(roomIsMute); notifyListeners(); } ///点击的位置 void clickSite(num index, {SocialChatUserProfile? clickUser}) { if (_handleDirectSeatInteraction(index, clickUser: clickUser)) { return; } if (index == -1) { if (clickUser != null) { if (clickUser.id == AccountStorage().getCurrentUser()?.userProfile?.id) { ///是自己,直接打开资料卡 showBottomInCenterDialog( context!, RoomUserInfoCard(userId: clickUser.id), ); } else { showBottomInBottomDialog( context!, EmptyMaiSelect(index: index, clickUser: clickUser), ); } } } else { SocialChatUserProfile? roomWheatUser = roomWheatMap[index]?.user; showBottomInBottomDialog( context!, EmptyMaiSelect(index: index, clickUser: roomWheatUser), ); // if (roomWheatUser != null) { // ///麦上有人 // if (roomWheatUser.id == // AccountStorage().getCurrentUser()?.userProfile?.id) { // ///是自己 // showBottomInBottomDialog( // context!, // EmptyMaiSelect(index: index, clickUser: roomWheatUser), // ); // } else { // showBottomInBottomDialog( // context!, // RoomUserInfoCard(userId: roomWheatUser.id), // ); // } // } else { // showBottomInBottomDialog( // context!, // EmptyMaiSelect(index: index, clickUser: roomWheatUser), // ); // } } } bool _handleDirectSeatInteraction( num index, { SocialChatUserProfile? clickUser, }) { final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id; final isRoomAdmin = isFz() || isGL(); if (index == -1) { if (clickUser == null || (clickUser.id ?? '').isEmpty) { return false; } if (clickUser.id == currentUserId || !isRoomAdmin) { _openRoomUserInfoCard(clickUser.id); return true; } return false; } final seat = roomWheatMap[index]; final seatUser = seat?.user; if (seatUser == null) { if (!(seat?.micLock ?? false) && !isRoomAdmin) { shangMai(index); return true; } return false; } if (seatUser.id == currentUserId) { if (isRoomAdmin) { return false; } _openRoomUserInfoCard(seatUser.id); return true; } if (!isRoomAdmin && (seatUser.id ?? '').isNotEmpty) { _openRoomUserInfoCard(seatUser.id); return true; } return false; } void _refreshMicListSilently() { retrieveMicrophoneList(notifyIfUnchanged: false).catchError((_) {}); } void _openRoomUserInfoCard(String? userId) { final normalizedUserId = (userId ?? '').trim(); if (normalizedUserId.isEmpty) { return; } showBottomInCenterDialog( context!, RoomUserInfoCard(userId: normalizedUserId), ); } addSoundVoiceChangeListener(OnSoundVoiceChange onSoundVoiceChange) { _onSoundVoiceChangeList.add(onSoundVoiceChange); print('添加监听:${_onSoundVoiceChangeList.length}'); } removeSoundVoiceChangeListener(OnSoundVoiceChange onSoundVoiceChange) { _onSoundVoiceChangeList.remove(onSoundVoiceChange); print('删除监听:${_onSoundVoiceChangeList.length}'); } void shangMai(num index, {String? eventType, String? inviterId}) async { var myUser = AccountStorage().getCurrentUser()?.userProfile; if (currenRoom?.roomProfile?.roomSetting?.touristMike ?? false) { } else { if (isTourists()) { SCTts.show( SCAppLocalizations.of(context!)!.touristsAreNotAllowedToGoOnTheMic, ); return; } } try { var micGoUpRes = await SCChatRoomRepository().micGoUp( currenRoom?.roomProfile?.roomProfile?.id ?? "", index, eventType: eventType, inviterId: inviterId, ); /// 设置成主播角色 engine?.renewToken(micGoUpRes.roomToken ?? ""); if (!micGoUpRes.micMute!) { if (!isMic) { engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster); adjustRecordingSignalVolume(100); await engine?.muteLocalAudioStream(false); } } else { engine?.setClientRole(role: ClientRoleType.clientRoleAudience); await engine?.muteLocalAudioStream(true); } SCHeartbeatUtils.scheduleAnchorHeartbeat( currenRoom?.roomProfile?.roomProfile?.id ?? "", ); final currentSeat = roomWheatMap[index]; if (currentSeat != null && myUser != null) { roomWheatMap[index] = currentSeat.copyWith( user: myUser.copyWith(roles: currenRoom?.entrants?.roles), micMute: micGoUpRes.micMute, roomToken: micGoUpRes.roomToken, ); } if (roomWheatMap[index]?.micMute ?? false) { ///房主上麦自动解禁麦位 if (isFz()) { await jieJinMai(index); } } notifyListeners(); _refreshMicListSilently(); } catch (ex) { SCTts.show('Failed to put on the microphone, $ex'); } } xiaMai(num index) async { try { await SCChatRoomRepository().micGoDown( currenRoom?.roomProfile?.roomProfile?.id ?? "", index, ); if (roomWheatMap[index]?.user?.id == AccountStorage().getCurrentUser()?.userProfile?.id) { isMic = true; engine?.muteLocalAudioStream(true); } SCHeartbeatUtils.cancelAnchorTimer(); /// 设置成主持人角色 engine?.renewToken(""); engine?.setClientRole(role: ClientRoleType.clientRoleAudience); final currentSeat = roomWheatMap[index]; if (currentSeat != null) { roomWheatMap[index] = currentSeat.copyWith(user: null); } notifyListeners(); _refreshMicListSilently(); } catch (ex) { SCTts.show('Failed to leave the microphone, $ex'); } } ///踢人下麦 killXiaMai(String userId) { try { roomWheatMap.forEach((k, v) async { if (v.user?.id == userId) { var canKill = await SCChatRoomRepository().kickOffMicrophone( currenRoom?.roomProfile?.roomProfile?.id ?? "", userId, ); if (canKill) { await SCChatRoomRepository().micKill( currenRoom?.roomProfile?.roomProfile?.id ?? "", k, ); Provider.of(context!, listen: false).dispatchMessage( Msg( groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount, msg: userId, type: SCRoomMsgType.killXiaMai, ), addLocal: false, ); roomWheatMap[k]?.setUser = null; notifyListeners(); } else { SCTts.show("cant kill the user"); } } }); } catch (e) { SCTts.show("kill the user fail"); print('踢人下麦失败'); } } ///自己是否在麦上 bool isOnMai() { return roomWheatMap.values .map((userWheat) => userWheat.user?.id) .toList() .contains(AccountStorage().getCurrentUser()?.userProfile?.id); } ///自己是否在指定的麦上 bool isOnMaiInIndex(num index) { return roomWheatMap[index]?.user?.id == AccountStorage().getCurrentUser()?.userProfile?.id; } ///点击的用户在哪个麦上 num userOnMaiInIndex(String userId) { num index = -1; roomWheatMap.forEach((k, value) { if (value.user?.id == userId) { index = k; } }); return index; } ///座位上的用户是否开麦 bool userOnMaiInIndexOfMute(String userId) { bool micMute = false; roomWheatMap.forEach((k, value) { if (value.user?.id == userId) { value.micMute; } }); return micMute; } ///是否是房主 bool isFz() { return currenRoom?.entrants?.roles == SCRoomRolesType.HOMEOWNER.name; } ///是否是管理 bool isGL() { return currenRoom?.entrants?.roles == SCRoomRolesType.ADMIN.name; } ///是否是会员 bool isHY() { return currenRoom?.entrants?.roles == SCRoomRolesType.MEMBER.name; } ///是否是游客 bool isTourists() { return currenRoom?.entrants?.roles == SCRoomRolesType.TOURIST.name; } ///麦位变动 void micChange(List? mics) { if (mics == null || mics.isEmpty) { return; } _applyMicSnapshot(_buildMicMap(mics)); final isOnMic = userOnMaiInIndex(AccountStorage().getCurrentUser()?.userProfile?.id ?? "") > -1; SCHeartbeatUtils.scheduleHeartbeat( SCHeartbeatStatus.VOICE_LIVE.name, isOnMic, roomId: currenRoom?.roomProfile?.roomProfile?.id, ); } ///找一个空麦位 -1表示没有空位 num findWheat() { for (var entry in roomWheatMap.entries) { if (entry.value.user == null && !entry.value.micLock!) { return entry.key; } } return -1; } Future loadRoomInfo(String roomId) async { try { debugPrint("[Room Cover Sync] loadRoomInfo start roomId=$roomId"); final value = await SCChatRoomRepository().specific(roomId); final currentRoomProfile = currenRoom?.roomProfile?.roomProfile; debugPrint( "[Room Cover Sync] loadRoomInfo fetched roomId=${value.id ?? roomId} roomCover=${value.roomCover ?? ""} roomName=${value.roomName ?? ""}", ); currenRoom = currenRoom?.copyWith( roomProfile: currenRoom?.roomProfile?.copyWith( roomCounter: value.counter ?? currenRoom?.roomProfile?.roomCounter, roomSetting: value.setting ?? currenRoom?.roomProfile?.roomSetting, roomProfile: currentRoomProfile?.copyWith( roomCover: _preferNonEmpty( value.roomCover, currentRoomProfile.roomCover, ), roomName: _preferNonEmpty( value.roomName, currentRoomProfile.roomName, ), roomDesc: _preferNonEmpty( value.roomDesc, currentRoomProfile.roomDesc, ), ), ), ); debugPrint( "[Room Cover Sync] loadRoomInfo applied roomId=$roomId roomCover=${currenRoom?.roomProfile?.roomProfile?.roomCover ?? ""}", ); ///如果是游客禁止上麦,已经在麦上的游客需要下麦 bool touristMike = currenRoom?.roomProfile?.roomSetting?.touristMike ?? false; if (!touristMike && isTourists()) { num index = userOnMaiInIndex( AccountStorage().getCurrentUser()?.userProfile?.id ?? "", ); if (index > -1) { xiaMai(index); } } notifyListeners(); return true; } catch (e) { await resetLocalRoomState(fallbackRtmProvider: rtmProvider); SCTts.show("Room not exists"); return false; } } ///解锁麦位 Future jieFeng(num index) async { await SCChatRoomRepository().micLock( currenRoom?.roomProfile?.roomProfile?.id ?? "", index, false, ); final mic = roomWheatMap[index]; if (mic != null) { roomWheatMap[index] = mic.copyWith(micLock: false); notifyListeners(); } _refreshMicListSilently(); } ///锁麦 Future fengMai(num index) async { await SCChatRoomRepository().micLock( currenRoom?.roomProfile?.roomProfile?.id ?? "", index, true, ); final mic = roomWheatMap[index]; if (mic != null) { roomWheatMap[index] = mic.copyWith(micLock: true); notifyListeners(); } _refreshMicListSilently(); } ///静音麦克风 Future jinMai(num index) async { await SCChatRoomRepository().micMute( currenRoom?.roomProfile?.roomProfile?.id ?? "", index, true, ); if (isOnMaiInIndex(index)) { Provider.of( context!, listen: false, ).engine?.setClientRole(role: ClientRoleType.clientRoleAudience); Provider.of( context!, listen: false, ).engine?.muteLocalAudioStream(true); } var mic = roomWheatMap[index]; if (mic != null) { roomWheatMap[index] = mic.copyWith(micMute: true); notifyListeners(); } _refreshMicListSilently(); } ///解除静音麦克风 Future jieJinMai(num index) async { await SCChatRoomRepository().micMute( currenRoom?.roomProfile?.roomProfile?.id ?? "", index, false, ); if (isOnMaiInIndex(index)) { if (!Provider.of(context!, listen: false).isMic) { if (roomWheatMap[index]?.user != null) { adjustRecordingSignalVolume(100); Provider.of( context!, listen: false, ).engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster); Provider.of( context!, listen: false, ).engine?.muteLocalAudioStream(false); } } } var mic = roomWheatMap[index]; if (mic != null) { roomWheatMap[index] = mic.copyWith(micMute: false); notifyListeners(); } _refreshMicListSilently(); } void addOnlineUser(String groupId, SocialChatUserProfile user) { if (groupId != currenRoom?.roomProfile?.roomProfile?.roomAccount) { return; } bool isExtOnlineList = false; for (var us in onlineUsers) { if (us.id == user.id) { isExtOnlineList = true; break; } } if (!isExtOnlineList) { Provider.of(context!, listen: false).onlineUsers.add(user); _refreshManagerUsers(onlineUsers); notifyListeners(); } } void removOnlineUser(String groupId, String userId) { if (groupId != currenRoom?.roomProfile?.roomProfile?.roomAccount) { return; } SocialChatUserProfile? isExtOnlineUser; for (var us in onlineUsers) { if (us.id == userId) { isExtOnlineUser = us; break; } } if (isExtOnlineUser != null) { Provider.of( context!, listen: false, ).onlineUsers.remove(isExtOnlineUser); _refreshManagerUsers(onlineUsers); notifyListeners(); } } void starPlayEmoji(Msg msg) { if (msg.number! > -1) { var mic = roomWheatMap[msg.number]; if (mic != null && mic.user != null) { roomWheatMap[msg.number!] = mic.copyWith( emojiPath: msg.msg, type: msg.type, number: msg.msg, ); notifyListeners(); } } } ///更新房间背景 void updateRoomBG(SCRoomThemeListRes res) { var roomTheme = RoomTheme( expireTime: res.expireTime, id: res.id, themeStatus: res.themeStatus, themeBack: res.themeBack, ); currenRoom?.roomProps?.setRoomTheme(roomTheme); notifyListeners(); } void updateRoomSetting(RoomSetting roomSetting) { currenRoom?.roomProfile?.setRoomSetting(roomSetting); notifyListeners(); } Map>> seatGlobalKeyMap = {}; void bindTargetKey(num index, GlobalKey> targetKey) { seatGlobalKeyMap[index] = targetKey; } GlobalKey>? getSeatGlobalKeyByIndex(String userId) { ///需要有人的座位 num index = userOnMaiInIndex(userId); if (index > -1) { return seatGlobalKeyMap[index]; } return null; } ///播放音乐 void startAudioMixing(String localPath, int cycle) { if (localPath.isEmpty) { return; } engine?.startAudioMixing( filePath: localPath, loopback: false, cycle: cycle, ); } void adjustRecordingSignalVolume(int volume) { engine?.adjustRecordingSignalVolume(volume); } }