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); static const Duration _selfMicStateGracePeriod = Duration(seconds: 4); static const Duration _giftTriggeredMicRefreshMinInterval = Duration( milliseconds: 900, ); bool needUpDataUserInfo = false; bool _roomVisualEffectsEnabled = false; bool _isExitingCurrentVoiceRoomSession = false; Timer? _micListPollingTimer; Timer? _onlineUsersPollingTimer; bool _isRefreshingMicList = false; bool _isRefreshingOnlineUsers = false; bool _pendingMicListRefresh = false; Timer? _deferredMicListRefreshTimer; int _lastMicListRefreshStartedAtMs = 0; num? _preferredSelfMicIndex; num? _pendingSelfMicSourceIndex; int? _pendingSelfMicSwitchGuardUntilMs; num? _pendingSelfMicReleaseIndex; int? _pendingSelfMicReleaseGuardUntilMs; ClientRoleType? _lastAppliedClientRole; bool? _lastAppliedLocalAudioMuted; bool? _lastScheduledVoiceLiveOnMic; String? _lastScheduledVoiceLiveRoomId; String? _lastScheduledAnchorRoomId; ///当前所在房间 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(); _deferredMicListRefreshTimer?.cancel(); _micListPollingTimer = null; _onlineUsersPollingTimer = null; _deferredMicListRefreshTimer = 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 = {}; final userSeatMap = {}; 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; final mergedMic = 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; final normalizedUserId = (mergedMic.user?.id ?? "").trim(); if (normalizedUserId.isNotEmpty) { final existingIndex = userSeatMap[normalizedUserId]; if (existingIndex != null) { final shouldReplaceExistingSeat = _shouldPreferDuplicateMicSeat( userId: normalizedUserId, existingIndex: existingIndex, candidateIndex: micIndex, previousMap: previousMap, ); if (!shouldReplaceExistingSeat) { continue; } nextMap.remove(existingIndex); } userSeatMap[normalizedUserId] = micIndex; } nextMap[micIndex] = mergedMic; } return _stabilizeSelfMicSnapshot(nextMap, previousMap: previousMap); } void _startSelfMicSwitchGuard({ required num sourceIndex, required num targetIndex, }) { _clearSelfMicReleaseGuard(); _preferredSelfMicIndex = targetIndex; _pendingSelfMicSourceIndex = sourceIndex; _pendingSelfMicSwitchGuardUntilMs = DateTime.now().add(_selfMicStateGracePeriod).millisecondsSinceEpoch; } void _clearSelfMicSwitchGuard({bool clearPreferredIndex = false}) { _pendingSelfMicSourceIndex = null; _pendingSelfMicSwitchGuardUntilMs = null; if (clearPreferredIndex) { _preferredSelfMicIndex = null; } } void _startSelfMicReleaseGuard({required num sourceIndex}) { _pendingSelfMicReleaseIndex = sourceIndex; _pendingSelfMicReleaseGuardUntilMs = DateTime.now().add(_selfMicStateGracePeriod).millisecondsSinceEpoch; } void _clearSelfMicReleaseGuard() { _pendingSelfMicReleaseIndex = null; _pendingSelfMicReleaseGuardUntilMs = null; } bool _shouldSuppressCurrentUserOnSeat(num index, {MicRes? seat}) { final currentUserId = (AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim(); if (currentUserId.isEmpty) { return false; } final releaseIndex = _pendingSelfMicReleaseIndex; final releaseGuardUntilMs = _pendingSelfMicReleaseGuardUntilMs ?? 0; if (releaseIndex != index || releaseGuardUntilMs <= DateTime.now().millisecondsSinceEpoch) { return false; } final resolvedSeat = seat ?? roomWheatMap[index]; return (resolvedSeat?.user?.id ?? "").trim() == currentUserId; } MicRes? micAtIndexForDisplay(num index) { final seat = roomWheatMap[index]; if (!_shouldSuppressCurrentUserOnSeat(index, seat: seat)) { return seat; } return seat?.copyWith(clearUser: true); } Map _stabilizeSelfMicSnapshot( Map nextMap, { required Map previousMap, }) { final currentUserId = (AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim(); if (currentUserId.isEmpty) { return nextMap; } final nowMs = DateTime.now().millisecondsSinceEpoch; final targetIndex = _preferredSelfMicIndex; final sourceIndex = _pendingSelfMicSourceIndex; final guardUntilMs = _pendingSelfMicSwitchGuardUntilMs ?? 0; final hasActiveGuard = targetIndex != null && sourceIndex != null && guardUntilMs > nowMs; final selfSeatIndices = nextMap.entries .where((entry) => entry.value.user?.id == currentUserId) .map((entry) => entry.key) .toList(); if (targetIndex != null && nextMap[targetIndex]?.user?.id == currentUserId) { _clearSelfMicSwitchGuard(); return nextMap; } if (!hasActiveGuard) { _clearSelfMicSwitchGuard(); } else { final resolvedTargetIndex = targetIndex; final resolvedSourceIndex = sourceIndex; final selfOnlyOnSource = selfSeatIndices.isEmpty || selfSeatIndices.every( (seatIndex) => seatIndex == resolvedSourceIndex, ); if (selfOnlyOnSource) { final optimisticTargetSeat = previousMap[resolvedTargetIndex]; if (optimisticTargetSeat != null && optimisticTargetSeat.user?.id == currentUserId) { final incomingTargetSeat = nextMap[resolvedTargetIndex]; if (incomingTargetSeat?.user == null || incomingTargetSeat?.user?.id == currentUserId) { final stabilizedMap = Map.from(nextMap); for (final seatIndex in selfSeatIndices) { if (seatIndex == resolvedTargetIndex) { continue; } final seat = stabilizedMap[seatIndex]; if (seat == null) { continue; } stabilizedMap[seatIndex] = seat.copyWith(clearUser: true); } final baseSeat = incomingTargetSeat ?? optimisticTargetSeat; stabilizedMap[resolvedTargetIndex] = baseSeat.copyWith( user: optimisticTargetSeat.user, micMute: incomingTargetSeat?.micMute ?? optimisticTargetSeat.micMute, micLock: incomingTargetSeat?.micLock ?? optimisticTargetSeat.micLock, roomToken: incomingTargetSeat?.roomToken ?? optimisticTargetSeat.roomToken, emojiPath: (incomingTargetSeat?.emojiPath ?? "").isNotEmpty ? incomingTargetSeat?.emojiPath : optimisticTargetSeat.emojiPath, type: (incomingTargetSeat?.type ?? "").isNotEmpty ? incomingTargetSeat?.type : optimisticTargetSeat.type, number: (incomingTargetSeat?.number ?? "").isNotEmpty ? incomingTargetSeat?.number : optimisticTargetSeat.number, ); return stabilizedMap; } } } } final releaseIndex = _pendingSelfMicReleaseIndex; final releaseGuardUntilMs = _pendingSelfMicReleaseGuardUntilMs ?? 0; final hasActiveReleaseGuard = releaseIndex != null && releaseGuardUntilMs > nowMs; if (!hasActiveReleaseGuard) { _clearSelfMicReleaseGuard(); return nextMap; } if (selfSeatIndices.isEmpty) { return nextMap; } if (selfSeatIndices.any((seatIndex) => seatIndex != releaseIndex)) { _clearSelfMicReleaseGuard(); return nextMap; } final releaseSeat = nextMap[releaseIndex]; if (releaseSeat?.user?.id != currentUserId) { _clearSelfMicReleaseGuard(); return nextMap; } final stabilizedMap = Map.from(nextMap); stabilizedMap[releaseIndex] = releaseSeat!.copyWith(clearUser: true); return stabilizedMap; } bool _shouldPreferDuplicateMicSeat({ required String userId, required num existingIndex, required num candidateIndex, required Map previousMap, }) { if (existingIndex == candidateIndex) { return true; } final currentUserId = (AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim(); if (userId == currentUserId && _preferredSelfMicIndex != null) { if (candidateIndex == _preferredSelfMicIndex) { return true; } if (existingIndex == _preferredSelfMicIndex) { return false; } } final candidateWasPreviouslyOccupiedByUser = previousMap[candidateIndex]?.user?.id == userId; final existingWasPreviouslyOccupiedByUser = previousMap[existingIndex]?.user?.id == userId; if (candidateWasPreviouslyOccupiedByUser != existingWasPreviouslyOccupiedByUser) { return candidateWasPreviouslyOccupiedByUser; } return false; } void _syncSelfMicRuntimeState() { final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id; if ((currentUserId ?? "").isEmpty) { return; } final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? ""; MicRes? currentUserMic; for (final mic in roomWheatMap.values) { if (mic.user?.id == currentUserId) { currentUserMic = mic; break; } } if (currentUserMic == null) { _clearSelfMicSwitchGuard(clearPreferredIndex: true); _syncRoomHeartbeatState( roomId: roomId, isOnMic: false, shouldRunAnchorHeartbeat: false, ); _applyLocalAudioRuntimeState( clientRole: ClientRoleType.clientRoleAudience, muted: true, ); return; } _preferredSelfMicIndex = currentUserMic.micIndex; _syncRoomHeartbeatState( roomId: roomId, isOnMic: true, shouldRunAnchorHeartbeat: true, ); if ((currentUserMic.micMute ?? false) || isMic) { _applyLocalAudioRuntimeState( clientRole: ClientRoleType.clientRoleAudience, muted: true, ); return; } _applyLocalAudioRuntimeState( clientRole: ClientRoleType.clientRoleBroadcaster, muted: false, ); } void _syncRoomHeartbeatState({ required String roomId, required bool isOnMic, required bool shouldRunAnchorHeartbeat, }) { final normalizedRoomId = roomId.trim(); if (normalizedRoomId.isEmpty) { _resetHeartbeatTracking(); return; } final voiceLiveChanged = _lastScheduledVoiceLiveRoomId != normalizedRoomId || _lastScheduledVoiceLiveOnMic != isOnMic; if (voiceLiveChanged) { SCHeartbeatUtils.scheduleHeartbeat( SCHeartbeatStatus.VOICE_LIVE.name, isOnMic, roomId: normalizedRoomId, ); _lastScheduledVoiceLiveRoomId = normalizedRoomId; _lastScheduledVoiceLiveOnMic = isOnMic; } if (!shouldRunAnchorHeartbeat) { if (_lastScheduledAnchorRoomId != null) { SCHeartbeatUtils.cancelAnchorTimer(); _lastScheduledAnchorRoomId = null; } return; } if (voiceLiveChanged || _lastScheduledAnchorRoomId != normalizedRoomId) { SCHeartbeatUtils.scheduleAnchorHeartbeat(normalizedRoomId); _lastScheduledAnchorRoomId = normalizedRoomId; } } void _applyLocalAudioRuntimeState({ required ClientRoleType clientRole, required bool muted, }) { if (engine == null) { _lastAppliedClientRole = null; _lastAppliedLocalAudioMuted = null; return; } if (_lastAppliedClientRole != clientRole) { engine?.setClientRole(role: clientRole); _lastAppliedClientRole = clientRole; } if (!muted && _lastAppliedLocalAudioMuted != false) { adjustRecordingSignalVolume(100); } if (_lastAppliedLocalAudioMuted != muted) { engine?.muteLocalAudioStream(muted); _lastAppliedLocalAudioMuted = muted; } } void _resetHeartbeatTracking() { _lastScheduledVoiceLiveOnMic = null; _lastScheduledVoiceLiveRoomId = null; _lastScheduledAnchorRoomId = null; } void _resetLocalAudioRuntimeTracking() { _lastAppliedClientRole = null; _lastAppliedLocalAudioMuted = null; } void _applyMicSnapshot( Map nextMap, { bool notifyIfUnchanged = true, }) { final changed = !_sameMicMaps(roomWheatMap, nextMap); roomWheatMap = nextMap; _syncSelfMicRuntimeState(); if (changed || notifyIfUnchanged) { notifyListeners(); } } int? _tryParseAgoraUid(String? value) { final normalizedValue = (value ?? '').trim(); if (normalizedValue.isEmpty) { return null; } return int.tryParse(normalizedValue); } int _stableAgoraUidFromUser(SocialChatUserProfile? user) { final source = '${user?.account ?? ""}|${user?.id ?? ""}|${user?.userNickname ?? ""}'; var hash = 0x811C9DC5; for (final codeUnit in source.codeUnits) { hash ^= codeUnit; hash = (hash * 0x01000193) & 0x7fffffff; } return hash == 0 ? 1 : hash; } int _resolveAgoraUidForUser(SocialChatUserProfile? user) { final parsedAccountUid = _tryParseAgoraUid(user?.account); if (parsedAccountUid != null && parsedAccountUid > 0) { return parsedAccountUid; } final parsedUserIdUid = _tryParseAgoraUid(user?.id); if (parsedUserIdUid != null && parsedUserIdUid > 0) { return parsedUserIdUid; } return _stableAgoraUidFromUser(user); } int _resolveAgoraUidForCurrentUser() { return _resolveAgoraUidForUser( AccountStorage().getCurrentUser()?.userProfile, ); } void requestGiftTriggeredMicRefresh() { requestMicrophoneListRefresh( notifyIfUnchanged: false, minInterval: _giftTriggeredMicRefreshMinInterval, ); } void requestMicrophoneListRefresh({ bool notifyIfUnchanged = false, Duration minInterval = Duration.zero, }) { final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? ""; if (roomId.isEmpty) { return; } if (minInterval <= Duration.zero) { _deferredMicListRefreshTimer?.cancel(); _deferredMicListRefreshTimer = null; retrieveMicrophoneList( notifyIfUnchanged: notifyIfUnchanged, ).catchError((_) {}); return; } final nowMs = DateTime.now().millisecondsSinceEpoch; final elapsedMs = nowMs - _lastMicListRefreshStartedAtMs; if (!_isRefreshingMicList && elapsedMs >= minInterval.inMilliseconds) { _deferredMicListRefreshTimer?.cancel(); _deferredMicListRefreshTimer = null; retrieveMicrophoneList( notifyIfUnchanged: notifyIfUnchanged, ).catchError((_) {}); return; } final remainingMs = minInterval.inMilliseconds - elapsedMs; final delayMs = remainingMs > 80 ? remainingMs : 80; _deferredMicListRefreshTimer?.cancel(); _deferredMicListRefreshTimer = Timer(Duration(milliseconds: delayMs), () { _deferredMicListRefreshTimer = null; retrieveMicrophoneList( notifyIfUnchanged: notifyIfUnchanged, ).catchError((_) {}); }); } 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: _resolveAgoraUidForCurrentUser(), options: ChannelMediaOptions( // 自动订阅所有视频流 autoSubscribeVideo: false, // 自动订阅所有音频流 autoSubscribeAudio: true, // 发布摄像头采集的视频 publishCameraTrack: false, // 发布麦克风采集的音频 publishMicrophoneTrack: true, // 设置用户角色为 clientRoleBroadcaster(主播)或 clientRoleAudience(观众) clientRoleType: ClientRoleType.clientRoleAudience, channelProfile: ChannelProfileType.channelProfileLiveBroadcasting, ), ); _syncSelfMicRuntimeState(); 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()}'); if (roomWheatMap.isEmpty || speakers.isEmpty) { return; } for (final seat in roomWheatMap.values) { seat.setVolume = 0; } final seatIndexByAgoraUid = {}; roomWheatMap.forEach((seatIndex, mic) { final user = mic.user; if (user == null) { return; } seatIndexByAgoraUid[_resolveAgoraUidForUser(user)] = seatIndex; }); final currentUserAgoraUid = _resolveAgoraUidForCurrentUser(); final listeners = List.from(_onSoundVoiceChangeList); for (final info in speakers) { final volume = info.volume ?? 0; if (volume <= 0) { continue; } final resolvedAgoraUid = info.uid == 0 ? currentUserAgoraUid : info.uid; final seatIndex = seatIndexByAgoraUid[resolvedAgoraUid]; if (seatIndex == null) { continue; } roomWheatMap[seatIndex]?.setVolume = volume; for (final listener in listeners) { listener(seatIndex, volume); } } } ///关注/取消房间 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) { return; } if (_isRefreshingMicList) { _pendingMicListRefresh = true; return; } _isRefreshingMicList = true; _lastMicListRefreshStartedAtMs = DateTime.now().millisecondsSinceEpoch; try { final roomWheatList = await SCChatRoomRepository().micList(roomId); if (roomId != currenRoom?.roomProfile?.roomProfile?.id) { return; } final nextMap = _buildMicMap(roomWheatList); _applyMicSnapshot(nextMap, notifyIfUnchanged: notifyIfUnchanged); } finally { _isRefreshingMicList = false; if (_pendingMicListRefresh) { _pendingMicListRefresh = false; unawaited( Future.microtask( () => retrieveMicrophoneList(notifyIfUnchanged: notifyIfUnchanged), ), ); } } } 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(); engine = null; _resetHeartbeatTracking(); _resetLocalAudioRuntimeTracking(); roomRocketStatus = null; rtmProvider ?.onNewMessageListenerGroupMap["${currenRoom?.roomProfile?.roomProfile?.roomAccount}"] = null; roomWheatMap.clear(); onlineUsers.clear(); managerUsers.clear(); needUpDataUserInfo = false; SCRoomUtils.roomUsersMap.clear(); _clearSelfMicSwitchGuard(clearPreferredIndex: true); _clearSelfMicReleaseGuard(); 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 _clearUserFromSeats(String? userId, {num? exceptIndex}) { final normalizedUserId = (userId ?? "").trim(); if (normalizedUserId.isEmpty) { return; } roomWheatMap.forEach((seatIndex, seat) { if (seat.user?.id != normalizedUserId || seatIndex == exceptIndex) { return; } roomWheatMap[seatIndex] = seat.copyWith(clearUser: true); }); } 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 targetIndex = micGoUpRes.micIndex ?? index; final previousSelfSeatIndex = myUser != null ? userOnMaiInIndex(myUser.id ?? "") : -1; final isSeatSwitching = previousSelfSeatIndex > -1 && previousSelfSeatIndex != targetIndex; if (myUser != null) { _clearSelfMicReleaseGuard(); if (isSeatSwitching) { _startSelfMicSwitchGuard( sourceIndex: previousSelfSeatIndex, targetIndex: targetIndex, ); } else { _preferredSelfMicIndex = targetIndex; _clearSelfMicSwitchGuard(); } _clearUserFromSeats(myUser.id, exceptIndex: targetIndex); final currentSeat = roomWheatMap[targetIndex]; final currentUser = myUser.copyWith(roles: currenRoom?.entrants?.roles); roomWheatMap[targetIndex] = currentSeat?.copyWith( user: currentUser, micMute: micGoUpRes.micMute, micLock: micGoUpRes.micLock, roomToken: micGoUpRes.roomToken, ) ?? MicRes( roomId: currenRoom?.roomProfile?.roomProfile?.id, micIndex: targetIndex, micLock: micGoUpRes.micLock, micMute: micGoUpRes.micMute, user: currentUser, roomToken: micGoUpRes.roomToken, ); } if (roomWheatMap[targetIndex]?.micMute ?? false) { ///房主上麦自动解禁麦位 if (isFz()) { await jieJinMai(targetIndex); } } 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, ); final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id ?? ""; final currentUserSeatIndex = userOnMaiInIndex(currentUserId); if (roomWheatMap[index]?.user?.id == currentUserId) { isMic = true; engine?.muteLocalAudioStream(true); } _clearSelfMicSwitchGuard(clearPreferredIndex: true); if (currentUserSeatIndex > -1) { _startSelfMicReleaseGuard(sourceIndex: currentUserSeatIndex); } else { _clearSelfMicReleaseGuard(); } SCHeartbeatUtils.cancelAnchorTimer(); /// 设置成主持人角色 engine?.renewToken(""); engine?.setClientRole(role: ClientRoleType.clientRoleAudience); _clearUserFromSeats(currentUserId); notifyListeners(); requestMicrophoneListRefresh( notifyIfUnchanged: false, minInterval: const Duration(milliseconds: 350), ); } 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() { final currentUserId = (AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim(); if (currentUserId.isEmpty) { return false; } for (final entry in roomWheatMap.entries) { if ((micAtIndexForDisplay(entry.key)?.user?.id ?? "").trim() == currentUserId) { return true; } } return false; } ///自己是否在指定的麦上 bool isOnMaiInIndex(num index) { return (micAtIndexForDisplay(index)?.user?.id ?? "").trim() == (AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim(); } ///点击的用户在哪个麦上 num userOnMaiInIndex(String userId) { final normalizedUserId = userId.trim(); if (normalizedUserId.isEmpty) { return -1; } num index = -1; roomWheatMap.forEach((k, value) { final visibleSeat = normalizedUserId == (AccountStorage().getCurrentUser()?.userProfile?.id ?? "") .trim() ? micAtIndexForDisplay(k) : value; if ((visibleSeat?.user?.id ?? "").trim() == normalizedUserId) { 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)); } ///找一个空麦位 -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); } }