import 'dart:async'; import 'package:dio/dio.dart'; import 'package:agora_rtc_engine/agora_rtc_engine.dart'; import 'package:flutter/material.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/shared/data_sources/sources/repositories/sc_vip_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/components/sc_rotating_dots_loading.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_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/follow_room_res.dart' as follow_room; import 'package:yumi/shared/business_logic/models/res/my_room_res.dart' as my_room; 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/business_logic/models/res/sc_vip_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; typedef RoomMusicMixingStateListener = void Function(AudioMixingStateType state, AudioMixingReasonType reason); typedef RoomMusicMicRouteChangedListener = void Function(bool isOnMic); enum RoomStartupStatus { idle, loading, ready, failed } enum RoomStartupFailureType { none, entry, im, rtc } class _RoomStartupAsyncResult { const _RoomStartupAsyncResult.value(this.value) : error = null, stackTrace = null; const _RoomStartupAsyncResult.error(this.error, this.stackTrace) : value = null; final T? value; final Object? error; final StackTrace? stackTrace; T requireValue() { final capturedError = error; if (capturedError != null) { Error.throwWithStackTrace( capturedError, stackTrace ?? StackTrace.current, ); } return value as T; } } class _AgoraJoinCanceled implements Exception { const _AgoraJoinCanceled(this.reason); final String reason; @override String toString() => reason; } enum _PendingRoomStartupSeatActionType { clickSeat, goUpMic } class RoomEntryPreviewData { const RoomEntryPreviewData({ this.roomId, this.roomAccount, this.userId, this.roomCover, this.roomBackground, this.roomName, this.roomDesc, this.event, this.sysOrigin, this.countryCode, this.countryName, this.nationalFlag, this.langCode, this.del, this.createTime, this.updateTime, this.activeTime, this.roomSetting, this.ownerProfile, this.roomAdminCount, this.roomMemberCount, this.entrantsRole, }); factory RoomEntryPreviewData.fromSocialChatRoom(SocialChatRoomRes room) { return RoomEntryPreviewData( roomId: room.id, roomAccount: room.roomAccount, userId: room.userId, roomCover: room.roomCover, roomBackground: room.roomBackground, roomName: room.roomName, roomDesc: room.roomDesc, event: room.event, sysOrigin: room.sysOrigin, countryCode: room.countryCode, countryName: room.countryName, nationalFlag: room.nationalFlag, roomSetting: room.extValues?.roomSetting, ownerProfile: room.userProfile, roomAdminCount: room.roomCounter?.adminCount, roomMemberCount: room.roomCounter?.memberCount, ); } factory RoomEntryPreviewData.fromFollowRoom(follow_room.FollowRoomRes room) { final roomProfile = room.roomProfile; return RoomEntryPreviewData( roomId: roomProfile?.id ?? room.id, roomAccount: roomProfile?.roomAccount, userId: roomProfile?.userId, roomCover: roomProfile?.roomCover, roomBackground: roomProfile?.roomBackground, roomName: roomProfile?.roomName, roomDesc: roomProfile?.roomDesc, event: roomProfile?.event, sysOrigin: roomProfile?.sysOrigin, countryCode: roomProfile?.countryCode, countryName: roomProfile?.countryName, nationalFlag: roomProfile?.nationalFlag, roomSetting: roomProfile?.extValues?.roomSetting, ownerProfile: roomProfile?.userProfile, roomAdminCount: roomProfile?.roomCounter?.adminCount, roomMemberCount: roomProfile?.roomCounter?.memberCount, ); } factory RoomEntryPreviewData.fromMyRoom( my_room.MyRoomRes room, { SocialChatUserProfile? ownerProfile, }) { return RoomEntryPreviewData( roomId: room.id, roomAccount: room.roomAccount, userId: room.userId, roomCover: room.roomCover, roomBackground: room.roomBackground, roomName: room.roomName, roomDesc: room.roomDesc, event: room.event, sysOrigin: room.sysOrigin, countryCode: room.countryCode, countryName: room.countryName, nationalFlag: room.nationalFlag, langCode: room.langCode, del: room.del, createTime: room.createTime, updateTime: room.updateTime, activeTime: room.activeTime, roomSetting: room.setting, ownerProfile: ownerProfile, roomAdminCount: room.counter?.adminCount, roomMemberCount: room.counter?.memberCount, entrantsRole: SCRoomRolesType.HOMEOWNER.name, ); } final String? roomId; final String? roomAccount; final String? userId; final String? roomCover; final String? roomBackground; final String? roomName; final String? roomDesc; final String? event; final String? sysOrigin; final String? countryCode; final String? countryName; final String? nationalFlag; final String? langCode; final bool? del; final num? createTime; final num? updateTime; final num? activeTime; final RoomSetting? roomSetting; final SocialChatUserProfile? ownerProfile; final num? roomAdminCount; final num? roomMemberCount; final String? entrantsRole; int? get seatCount => roomSetting?.mikeSize?.toInt(); JoinRoomRes toJoinRoomRes({String? fallbackRoomId}) { final resolvedRoomId = (roomId ?? "").trim().isNotEmpty ? roomId : fallbackRoomId; return JoinRoomRes( entrants: (entrantsRole ?? "").trim().isNotEmpty ? Entrants(roles: entrantsRole) : null, roomProfile: RoomProfile( regionCode: countryCode, roomCounter: roomAdminCount != null || roomMemberCount != null ? RoomCounter( adminCount: roomAdminCount, memberCount: roomMemberCount, ) : null, roomProfile: RoomProfile2( activeTime: activeTime, countryCode: countryCode, countryName: countryName, createTime: createTime, del: del, event: event, id: resolvedRoomId, langCode: langCode, nationalFlag: nationalFlag, roomAccount: roomAccount, roomCover: roomCover, roomBackground: roomBackground, roomDesc: roomDesc, roomName: roomName, sysOrigin: sysOrigin, updateTime: updateTime, userId: userId, ), roomSetting: roomSetting, userProfile: ownerProfile, userSVipLevel: ownerProfile?.vipLevel, ), ); } } class _PendingRoomStartupSeatAction { const _PendingRoomStartupSeatAction.clickSeat({ required this.index, this.clickUser, }) : type = _PendingRoomStartupSeatActionType.clickSeat, eventType = null, inviterId = null; const _PendingRoomStartupSeatAction.goUpMic({ required this.index, this.eventType, this.inviterId, }) : type = _PendingRoomStartupSeatActionType.goUpMic, clickUser = null; final _PendingRoomStartupSeatActionType type; final num index; final SocialChatUserProfile? clickUser; final String? eventType; final String? inviterId; } class RealTimeCommunicationManager extends ChangeNotifier { static const Duration _micListPollingInterval = Duration(seconds: 2); static const Duration _onlineUsersPollingInterval = Duration(seconds: 3); static const int _exitNetworkRetryLimit = 3; static const Duration _exitNetworkRetryDelay = Duration(milliseconds: 500); static const Duration _selfMicStateGracePeriod = Duration(seconds: 4); static const Duration _giftTriggeredMicRefreshMinInterval = Duration( milliseconds: 900, ); static const Duration _agoraJoinTimeout = Duration(seconds: 8); static const Duration _agoraDisconnectedGracePeriod = Duration(seconds: 12); static const String _roomStartupSeatLoadingTag = 'roomStartupSeatInteractionLoading'; static const String _selfMicGoUpRetryLoadingTag = 'selfMicGoUpRetryLoading'; static const int _selfMicGoUpFailureLimit = 3; bool needUpDataUserInfo = false; bool _roomVisualEffectsEnabled = false; bool _isExitingCurrentVoiceRoomSession = false; bool _isHandlingAgoraRoomFailure = false; bool _agoraJoined = false; bool _agoraRoleIsBroadcaster = false; String? _agoraJoinedChannelId; bool _agoraAudioConfigured = false; RoomStartupStatus _roomStartupStatus = RoomStartupStatus.idle; RoomStartupFailureType _roomStartupFailureType = RoomStartupFailureType.none; int _roomStartupFailureToken = 0; int _roomEntryRequestSerial = 0; int? _previewRoomSeatCount; bool _currentRoomIsEntryPreview = false; bool _isHandlingRoomStartupFailure = false; Timer? _micListPollingTimer; Timer? _onlineUsersPollingTimer; Timer? _joinAgoraTimeoutTimer; Timer? _agoraDisconnectedCleanupTimer; Timer? _roomEntryEffectTimer; Completer? _joinAgoraCompleter; Future? _rtcEngineInitTask; Future? _rtcEnginePrewarmTask; Future? _pendingRoomSwitchAgoraLeaveTask; String? _joiningChannelId; bool _isRefreshingMicList = false; bool _isRefreshingOnlineUsers = false; bool _disableMicListRefreshForCurrentSession = false; bool _disableOnlineUsersRefreshForCurrentSession = false; bool _isSelfMicActionInFlight = false; bool _roomStartupSeatLoadingVisible = false; bool _selfMicGoUpRetryLoadingVisible = false; int _selfMicGoUpConsecutiveFailureCount = 0; _PendingRoomStartupSeatAction? _pendingRoomStartupSeatAction; bool _pendingMicListRefresh = false; bool _pendingMicListRefreshNotifyIfUnchanged = 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 = {}; final Map _seatEmojiEventVersions = {}; int _emojiPlaybackEventVersion = 0; RtcEngine? engine; RtcEngineEventHandler? _rtcEngineEventHandler; RoomMusicMixingStateListener? _roomMusicMixingStateListener; RoomMusicMicRouteChangedListener? _roomMusicMicRouteChangedListener; BuildContext? context; RtmProvider? rtmProvider; SCIsFollowRoomRes? isFollowRoomRes; SCRoomRocketStatusRes? roomRocketStatus; ///房间是否静音 bool roomIsMute = false; bool closeFullGame = false; ///禁音开关 默认关闭 bool isMic = true; bool shouldShowSeatSoundWave(num index) { final seat = roomWheatMap[index]; final userId = (seat?.user?.id ?? "").trim(); if (userId.isEmpty || (seat?.micMute ?? false)) { return false; } final currentUserId = (AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim(); if (userId == currentUserId && isMic) { return false; } return 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; RoomStartupStatus get roomStartupStatus => _roomStartupStatus; RoomStartupFailureType get roomStartupFailureType => _roomStartupFailureType; int get roomStartupFailureToken => _roomStartupFailureToken; int? get previewRoomSeatCount => _previewRoomSeatCount; bool get shouldShowRoomVisualEffects => currenRoom != null && _roomVisualEffectsEnabled; bool get _shouldDeferSeatInteractionForRoomStartup => _roomStartupStatus == RoomStartupStatus.loading; void setRoomVisualEffectsEnabled(bool enabled) { if (_roomVisualEffectsEnabled == enabled) { return; } if (!enabled) { _roomEntryEffectTimer?.cancel(); _roomEntryEffectTimer = null; } _roomVisualEffectsEnabled = enabled; notifyListeners(); } void _setRoomStartupLoading() { _isHandlingRoomStartupFailure = false; _roomStartupFailureType = RoomStartupFailureType.none; if (_roomStartupStatus == RoomStartupStatus.loading) { return; } _roomStartupStatus = RoomStartupStatus.loading; notifyListeners(); } void _setRoomStartupReady() { _isHandlingRoomStartupFailure = false; _roomStartupFailureType = RoomStartupFailureType.none; if (_roomStartupStatus == RoomStartupStatus.ready) { return; } _roomStartupStatus = RoomStartupStatus.ready; notifyListeners(); _finishRoomStartupSeatLoading(executePendingAction: true); } void _setRoomStartupFailed(RoomStartupFailureType failureType) { if (_roomStartupStatus != RoomStartupStatus.loading) { return; } _stopRoomStatePolling(); _isHandlingRoomStartupFailure = false; _roomStartupFailureType = failureType; _roomStartupFailureToken += 1; _roomStartupStatus = RoomStartupStatus.failed; _finishRoomStartupSeatLoading(executePendingAction: false); notifyListeners(); } void _setExitingCurrentVoiceRoomSession(bool enabled, {bool notify = true}) { if (_isExitingCurrentVoiceRoomSession == enabled) { return; } _isExitingCurrentVoiceRoomSession = enabled; if (notify) { notifyListeners(); } } void _deferRoomStartupSeatAction(_PendingRoomStartupSeatAction action) { _pendingRoomStartupSeatAction = action; _showRoomStartupSeatLoading(); } void _showRoomStartupSeatLoading() { if (_roomStartupSeatLoadingVisible) { return; } _roomStartupSeatLoadingVisible = true; SmartDialog.dismiss(tag: _roomStartupSeatLoadingTag); SmartDialog.show( tag: _roomStartupSeatLoadingTag, alignment: Alignment.center, animationType: SmartAnimationType.fade, backType: SmartBackType.block, clickMaskDismiss: false, maskColor: Colors.transparent, builder: (_) { return Center( child: Container( width: 55, height: 55, decoration: BoxDecoration( color: Colors.black26, borderRadius: BorderRadius.circular(8), ), child: const Center(child: SCRotatingDotsLoading(size: 28)), ), ); }, ); } void _showSelfMicGoUpRetryLoading() { if (_selfMicGoUpRetryLoadingVisible) { return; } _selfMicGoUpRetryLoadingVisible = true; SmartDialog.dismiss(tag: _selfMicGoUpRetryLoadingTag); SmartDialog.show( tag: _selfMicGoUpRetryLoadingTag, alignment: Alignment.center, animationType: SmartAnimationType.fade, backType: SmartBackType.normal, clickMaskDismiss: true, maskColor: Colors.transparent, onDismiss: () { _selfMicGoUpRetryLoadingVisible = false; }, builder: (_) { return Center( child: Container( width: 55, height: 55, decoration: BoxDecoration( color: Colors.black26, borderRadius: BorderRadius.circular(8), ), child: const Center(child: SCRotatingDotsLoading(size: 28)), ), ); }, ); } void _dismissSelfMicGoUpRetryLoading() { if (!_selfMicGoUpRetryLoadingVisible) { return; } _selfMicGoUpRetryLoadingVisible = false; SmartDialog.dismiss(tag: _selfMicGoUpRetryLoadingTag); } void _resetSelfMicGoUpFailureState() { _selfMicGoUpConsecutiveFailureCount = 0; _dismissSelfMicGoUpRetryLoading(); } void _handleSelfMicGoUpFailure(String message) { _selfMicGoUpConsecutiveFailureCount += 1; if (_selfMicGoUpConsecutiveFailureCount >= _selfMicGoUpFailureLimit) { SCTts.show( 'Failed to put on the microphone after multiple attempts, please try again later', ); return; } SCTts.show(message); } void _finishRoomStartupSeatLoading({required bool executePendingAction}) { final pendingAction = _pendingRoomStartupSeatAction; _pendingRoomStartupSeatAction = null; if (_roomStartupSeatLoadingVisible) { _roomStartupSeatLoadingVisible = false; SmartDialog.dismiss(tag: _roomStartupSeatLoadingTag); } if (!executePendingAction || pendingAction == null) { return; } Future.microtask(() { if (_roomStartupStatus != RoomStartupStatus.ready) { return; } switch (pendingAction.type) { case _PendingRoomStartupSeatActionType.clickSeat: clickSite(pendingAction.index, clickUser: pendingAction.clickUser); break; case _PendingRoomStartupSeatActionType.goUpMic: shangMai( pendingAction.index, eventType: pendingAction.eventType, inviterId: pendingAction.inviterId, ); break; } }); } 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 _isMissingPathError(Object error) { final normalizedMessage = _errorMessageFrom(error).toLowerCase(); return normalizedMessage.contains('path was not found') || normalizedMessage.contains('the path was not found'); } String _errorMessageFrom(Object error) { if (error is DioException) { final responseData = error.response?.data; if (responseData is Map) { final dynamic errorMsg = responseData["errorMsg"]; if (errorMsg != null) { return errorMsg.toString(); } } if ((error.message ?? "").trim().isNotEmpty) { return error.message!; } } return error.toString(); } bool _isCancelledRequest(Object error) { if (error is DioException) { return error.type == DioExceptionType.cancel || (error.error?.toString().trim().toLowerCase() == 'cancel'); } final normalizedMessage = error.toString().toLowerCase(); return normalizedMessage.contains('dioexceptiontype.cancel') || normalizedMessage.contains('error: cancel') || normalizedMessage.contains('request was manually cancelled'); } 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.vipLevel == next.vipLevel && 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 hydratedMic = _mergeKnownVipIntoMic(mergedMic); final normalizedUserId = (hydratedMic.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] = hydratedMic; } 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); } int emojiEventVersionForSeat(num index) { return _seatEmojiEventVersions[index] ?? 0; } void consumeSeatEmojiEvent(num index, int eventVersion) { if (emojiEventVersionForSeat(index) != eventVersion) { return; } _seatEmojiEventVersions.remove(index); roomWheatMap[index]?.setEmojiPath = null; } 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; } bool _isCurrentAgoraChannel(String? channelId) { final normalizedChannelId = (channelId ?? "").trim(); final currentRoomId = (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(); if (normalizedChannelId.isEmpty) { return false; } return normalizedChannelId == currentRoomId || normalizedChannelId == (_joiningChannelId ?? "").trim(); } void _cancelAgoraJoinWaiter([Object? error]) { _joinAgoraTimeoutTimer?.cancel(); _joinAgoraTimeoutTimer = null; final completer = _joinAgoraCompleter; _joinAgoraCompleter = null; if (error != null && completer != null && !completer.isCompleted) { completer.completeError(error); } } void _cancelAgoraDisconnectedCleanup() { _agoraDisconnectedCleanupTimer?.cancel(); _agoraDisconnectedCleanupTimer = null; } void _resetAgoraTracking({bool clearJoiningChannel = true}) { _cancelAgoraJoinWaiter(const _AgoraJoinCanceled('Agora join canceled')); _cancelAgoraDisconnectedCleanup(); _agoraJoined = false; _agoraRoleIsBroadcaster = false; _agoraJoinedChannelId = null; if (clearJoiningChannel) { _joiningChannelId = null; } _resetLocalAudioRuntimeTracking(); } bool get _canReportMicActive { return _agoraJoined && _agoraRoleIsBroadcaster && isOnMai(); } bool get _joinAgoraCompleterIsPending { final completer = _joinAgoraCompleter; return completer != null && !completer.isCompleted; } void _resyncHeartbeatFromAgoraState() { final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? ""; final canReportMicActive = _canReportMicActive; _syncRoomHeartbeatState( roomId: roomId, isOnMic: canReportMicActive, shouldRunAnchorHeartbeat: canReportMicActive, ); } bool _isFatalAgoraError(ErrorCodeType err) { return err == ErrorCodeType.errInvalidArgument || err == ErrorCodeType.errNotReady || err == ErrorCodeType.errNotSupported || err == ErrorCodeType.errRefused || err == ErrorCodeType.errInvalidState || err == ErrorCodeType.errNoPermission || err == ErrorCodeType.errJoinChannelRejected || err == ErrorCodeType.errInvalidAppId || err == ErrorCodeType.errInvalidChannelName || err == ErrorCodeType.errTokenExpired || err == ErrorCodeType.errInvalidToken || err == ErrorCodeType.errConnectionLost; } bool _isFatalConnectionReason(ConnectionChangedReasonType reason) { return reason == ConnectionChangedReasonType.connectionChangedBannedByServer || reason == ConnectionChangedReasonType.connectionChangedJoinFailed || reason == ConnectionChangedReasonType.connectionChangedInvalidAppId || reason == ConnectionChangedReasonType.connectionChangedInvalidChannelName || reason == ConnectionChangedReasonType.connectionChangedInvalidToken || reason == ConnectionChangedReasonType.connectionChangedTokenExpired || reason == ConnectionChangedReasonType.connectionChangedRejectedByServer || reason == ConnectionChangedReasonType.connectionChangedSameUidLogin || reason == ConnectionChangedReasonType.connectionChangedTooManyBroadcasters || reason == ConnectionChangedReasonType .connectionChangedLicenseValidationFailure; } 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); _agoraRoleIsBroadcaster = false; _syncRoomHeartbeatState( roomId: roomId, isOnMic: false, shouldRunAnchorHeartbeat: false, ); _applyLocalAudioRuntimeState( clientRole: ClientRoleType.clientRoleAudience, muted: true, ); _roomMusicMicRouteChangedListener?.call(false); return; } _preferredSelfMicIndex = currentUserMic.micIndex; final canReportMicActive = _canReportMicActive; _syncRoomHeartbeatState( roomId: roomId, isOnMic: canReportMicActive, shouldRunAnchorHeartbeat: canReportMicActive, ); _applyLocalAudioRuntimeState( clientRole: ClientRoleType.clientRoleBroadcaster, muted: (currentUserMic.micMute ?? false) || isMic, ); _roomMusicMicRouteChangedListener?.call(true); } void _syncRoomHeartbeatState({ required String roomId, required bool isOnMic, required bool shouldRunAnchorHeartbeat, }) { final normalizedRoomId = roomId.trim(); if (normalizedRoomId.isEmpty) { _resetHeartbeatTracking(); return; } if (!_agoraJoined) { if (_lastScheduledVoiceLiveRoomId != null || _lastScheduledAnchorRoomId != null) { SCHeartbeatUtils.scheduleHeartbeat( SCHeartbeatStatus.ONLINE.name, false, ); SCHeartbeatUtils.cancelAnchorTimer(); } _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; } } Future _bootstrapRoomHeartbeatState() async { final String roomId = (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(); if (roomId.isEmpty) { _resetHeartbeatTracking(); return; } await SCHeartbeatUtils.scheduleHeartbeat( SCHeartbeatStatus.VOICE_LIVE.name, false, roomId: roomId, ); _lastScheduledVoiceLiveRoomId = roomId; _lastScheduledVoiceLiveOnMic = false; if (_lastScheduledAnchorRoomId != null) { SCHeartbeatUtils.cancelAnchorTimer(); _lastScheduledAnchorRoomId = null; } } void _applyLocalAudioRuntimeState({ required ClientRoleType clientRole, required bool muted, }) { if (clientRole != ClientRoleType.clientRoleBroadcaster) { _agoraRoleIsBroadcaster = false; } 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 || _disableMicListRefreshForCurrentSession) { 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 _cleanupAgoraRoomState({ bool clearMicSeats = false, String? onlyIfJoinedChannelId, }) async { final expectedChannelId = (onlyIfJoinedChannelId ?? "").trim(); if (expectedChannelId.isNotEmpty) { final joinedChannelId = (_agoraJoinedChannelId ?? "").trim(); final currentRoomId = (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(); if (joinedChannelId.isNotEmpty && joinedChannelId != expectedChannelId) { return; } if (joinedChannelId.isEmpty && currentRoomId.isNotEmpty && currentRoomId != expectedChannelId) { return; } } _resetAgoraTracking(); try { await engine?.leaveChannel(); } catch (error) { debugPrint('[Agora] leave channel during cleanup failed: $error'); } if (clearMicSeats) { roomWheatMap.clear(); notifyListeners(); } _resyncHeartbeatFromAgoraState(); } Future _leaveAgoraRoomForExit() async { final rtcEngine = engine; _resetAgoraTracking(); if (rtcEngine == null) { return; } try { await rtcEngine.leaveChannel(); } catch (error) { debugPrint('[RoomExit] leave Agora channel failed: $error'); } } Future _handleAgoraJoinFailed( Object error, { required bool exitRoom, }) async { debugPrint('[Agora] join failed: $error'); await _cleanupAgoraRoomState(clearMicSeats: true); if (!exitRoom || _isExitingCurrentVoiceRoomSession) { return; } SCTts.show("Join room fail"); unawaited(exitCurrentVoiceRoomSession(false)); } Future _handleAgoraDisconnected(Object reason) async { if (_isHandlingAgoraRoomFailure || _isExitingCurrentVoiceRoomSession) { return; } _isHandlingAgoraRoomFailure = true; debugPrint('[Agora] disconnected, leave room: $reason'); SCTts.show("Voice connection failed"); await _cleanupAgoraRoomState(clearMicSeats: true); await exitCurrentVoiceRoomSession(false); } void _scheduleAgoraDisconnectedCleanup(Object reason) { if (_isExitingCurrentVoiceRoomSession) { return; } _agoraDisconnectedCleanupTimer?.cancel(); _agoraDisconnectedCleanupTimer = Timer(_agoraDisconnectedGracePeriod, () { _agoraDisconnectedCleanupTimer = null; if (!_agoraJoined && !_isExitingCurrentVoiceRoomSession) { unawaited(_handleAgoraDisconnected(reason)); } }); } Future prewarmRtcEngine() { final existingTask = _rtcEnginePrewarmTask; if (existingTask != null) { return existingTask; } final task = _prewarmRtcEngine(); _rtcEnginePrewarmTask = task; return task; } Future _prewarmRtcEngine() async { try { final rtcEngine = await _initAgoraRtcEngine(); await _configureAgoraAudioEngine(rtcEngine); } catch (error, stackTrace) { _rtcEnginePrewarmTask = null; debugPrint('[Agora] prewarm failed: $error\n$stackTrace'); rethrow; } } Future _fetchRtcTokenForCurrentRoom({ required bool isPublisher, }) async { final channelId = (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(); final userId = (AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim(); final rtcToken = await SCAccountRepository().getRtcToken( channelId, userId, isPublisher: isPublisher, ); return rtcToken.rtcToken ?? ""; } Future<_RoomStartupAsyncResult> _captureStartupFuture( Future future, ) async { try { return _RoomStartupAsyncResult.value(await future); } catch (error, stackTrace) { return _RoomStartupAsyncResult.error(error, stackTrace); } } Future joinAgoraVoiceChannel({ bool throwOnError = false, String? rtcToken, }) async { final channelId = (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(); if (channelId.isEmpty) { final error = StateError('Agora channel id is empty'); await _handleAgoraJoinFailed(error, exitRoom: !throwOnError); if (throwOnError) { throw error; } return; } final pendingRoomSwitchLeaveTask = _pendingRoomSwitchAgoraLeaveTask; if (pendingRoomSwitchLeaveTask != null) { try { await pendingRoomSwitchLeaveTask; } catch (error) { debugPrint('[Agora] pending room switch leave failed: $error'); } } _cancelAgoraJoinWaiter(const _AgoraJoinCanceled('Agora join superseded')); _cancelAgoraDisconnectedCleanup(); _joiningChannelId = channelId; _agoraJoined = false; _agoraRoleIsBroadcaster = false; final joinCompleter = Completer(); _joinAgoraCompleter = joinCompleter; _joinAgoraTimeoutTimer = Timer(_agoraJoinTimeout, () { if (!joinCompleter.isCompleted) { joinCompleter.completeError( TimeoutException('Agora join timeout', _agoraJoinTimeout), ); } }); try { final rtcEngine = await _initAgoraRtcEngine(); await _configureAgoraAudioEngine(rtcEngine); final resolvedRtcToken = rtcToken ?? await _fetchRtcTokenForCurrentRoom(isPublisher: false); await rtcEngine.joinChannel( token: resolvedRtcToken, channelId: channelId, uid: _resolveAgoraUidForCurrentUser(), options: const ChannelMediaOptions( // 自动订阅所有视频流 autoSubscribeVideo: false, // 自动订阅所有音频流 autoSubscribeAudio: true, // 发布摄像头采集的视频 publishCameraTrack: false, // 发布麦克风采集的音频 publishMicrophoneTrack: true, // 设置用户角色为 clientRoleBroadcaster(主播)或 clientRoleAudience(观众) clientRoleType: ClientRoleType.clientRoleAudience, channelProfile: ChannelProfileType.channelProfileLiveBroadcasting, ), ); await joinCompleter.future; _syncSelfMicRuntimeState(); rtcEngine.muteAllRemoteAudioStreams(roomIsMute); } catch (e) { if (e is _AgoraJoinCanceled) { debugPrint('[Agora] join canceled: $e'); if (throwOnError) { rethrow; } return; } await _handleAgoraJoinFailed(e, exitRoom: !throwOnError); if (throwOnError) { rethrow; } debugPrint('加入失败:${e.runtimeType},${e.toString()}'); } finally { if (identical(_joinAgoraCompleter, joinCompleter)) { _cancelAgoraJoinWaiter(); } } } Future _switchAgoraToBroadcaster({ required String publisherToken, required bool muted, }) async { if (!_agoraJoined || engine == null) { return false; } try { if (publisherToken.trim().isNotEmpty) { await engine?.renewToken(publisherToken); } await engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster); if (!muted) { adjustRecordingSignalVolume(100); } await engine?.muteLocalAudioStream(muted); _lastAppliedClientRole = ClientRoleType.clientRoleBroadcaster; _lastAppliedLocalAudioMuted = muted; _agoraRoleIsBroadcaster = true; _resyncHeartbeatFromAgoraState(); return true; } catch (error) { debugPrint('[Agora] switch to broadcaster failed: $error'); _agoraRoleIsBroadcaster = false; _resyncHeartbeatFromAgoraState(); return false; } } Future _switchAgoraToAudience() async { try { await engine?.setClientRole(role: ClientRoleType.clientRoleAudience); await engine?.muteLocalAudioStream(true); _lastAppliedClientRole = ClientRoleType.clientRoleAudience; _lastAppliedLocalAudioMuted = true; _agoraRoleIsBroadcaster = false; _resyncHeartbeatFromAgoraState(); return true; } catch (error) { debugPrint('[Agora] switch to audience failed: $error'); return false; } } Future setSelfMicMutedFromBottom(bool muted) async { isMic = muted; notifyListeners(); final currentUserId = (AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim(); final seatIndex = userOnMaiInIndex(currentUserId); if (seatIndex < 0) { notifyListeners(); return; } final seat = roomWheatMap[seatIndex]; final seatMuted = seat?.micMute ?? false; if (seatMuted || muted) { if (_agoraJoined && isOnMai()) { await _switchAgoraToBroadcaster( publisherToken: seat?.roomToken ?? "", muted: true, ); } } else { await _switchAgoraToBroadcaster( publisherToken: seat?.roomToken ?? "", muted: false, ); } notifyListeners(); } Future handleSelfMicRemovedByRemote({ bool refreshMicList = true, }) async { final currentUserId = (AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim(); if (currentUserId.isEmpty) { await _switchAgoraToAudience(); return; } final currentSeatIndex = userOnMaiInIndex(currentUserId); isMic = true; _clearSelfMicSwitchGuard(clearPreferredIndex: true); if (currentSeatIndex > -1) { _startSelfMicReleaseGuard(sourceIndex: currentSeatIndex); } else { _clearSelfMicReleaseGuard(); } _clearUserFromSeats(currentUserId); _agoraRoleIsBroadcaster = false; _syncSelfMicRuntimeState(); notifyListeners(); await _switchAgoraToAudience(); if (refreshMicList) { requestMicrophoneListRefresh( notifyIfUnchanged: false, minInterval: const Duration(milliseconds: 350), ); } } Future _initAgoraRtcEngine() { if (engine != null) { return Future.value(engine!); } final existingTask = _rtcEngineInitTask; if (existingTask != null) { return existingTask; } final task = _createAgoraRtcEngine(); _rtcEngineInitTask = task; void clearInitTask() { if (identical(_rtcEngineInitTask, task)) { _rtcEngineInitTask = null; } } unawaited( task.then( (_) => clearInitTask(), onError: (_, __) => clearInitTask(), ), ); return task; } Future _createAgoraRtcEngine() async { final rtcEngine = createAgoraRtcEngine(); await rtcEngine.initialize( RtcEngineContext(appId: SCGlobalConfig.agoraRtcAppid), ); engine = rtcEngine; _rtcEngineEventHandler = RtcEngineEventHandler( onError: (ErrorCodeType err, String msg) { debugPrint('rtc错误$err'); final error = 'Agora error: $err $msg'; if (!_agoraJoined) { final completer = _joinAgoraCompleter; if (completer != null && !completer.isCompleted) { completer.completeError(StateError(error)); } else { unawaited(_handleAgoraJoinFailed(error, exitRoom: true)); } return; } if (_isFatalAgoraError(err)) { unawaited(_handleAgoraDisconnected(error)); } }, onJoinChannelSuccess: (RtcConnection connection, int elapsed) { debugPrint('rtc 自己加入 ${connection.channelId} ${connection.localUid}'); final joinedChannelId = connection.channelId ?? ""; if (!_isCurrentAgoraChannel(joinedChannelId)) { return; } _agoraJoined = true; _agoraRoleIsBroadcaster = false; _agoraJoinedChannelId = joinedChannelId.trim(); _cancelAgoraDisconnectedCleanup(); _joinAgoraTimeoutTimer?.cancel(); _joinAgoraTimeoutTimer = null; final completer = _joinAgoraCompleter; if (completer != null && !completer.isCompleted) { completer.complete(); } _resyncHeartbeatFromAgoraState(); }, onLeaveChannel: (RtcConnection connection, RtcStats stats) { if (_isCurrentAgoraChannel(connection.channelId)) { final completer = _joinAgoraCompleter; if (completer != null && !completer.isCompleted) { completer.completeError( StateError('Agora left channel before join success'), ); } _resetAgoraTracking(); _resyncHeartbeatFromAgoraState(); } }, onClientRoleChanged: ( RtcConnection connection, ClientRoleType oldRole, ClientRoleType newRole, ClientRoleOptions newRoleOptions, ) { if (!_isCurrentAgoraChannel(connection.channelId)) { return; } _agoraRoleIsBroadcaster = newRole == ClientRoleType.clientRoleBroadcaster; _resyncHeartbeatFromAgoraState(); }, onClientRoleChangeFailed: ( RtcConnection connection, ClientRoleChangeFailedReason reason, ClientRoleType currentRole, ) { if (!_isCurrentAgoraChannel(connection.channelId)) { return; } _agoraRoleIsBroadcaster = currentRole == ClientRoleType.clientRoleBroadcaster; _resyncHeartbeatFromAgoraState(); }, onConnectionStateChanged: ( RtcConnection connection, ConnectionStateType state, ConnectionChangedReasonType reason, ) { if (!_isCurrentAgoraChannel(connection.channelId)) { return; } if (state == ConnectionStateType.connectionStateConnected && reason == ConnectionChangedReasonType.connectionChangedRejoinSuccess) { _agoraJoined = true; _agoraJoinedChannelId = connection.channelId?.trim(); _cancelAgoraDisconnectedCleanup(); _resyncHeartbeatFromAgoraState(); return; } if (reason == ConnectionChangedReasonType.connectionChangedLeaveChannel) { _resetAgoraTracking(); _resyncHeartbeatFromAgoraState(); return; } if (state == ConnectionStateType.connectionStateFailed || _isFatalConnectionReason(reason)) { _agoraJoined = false; _agoraRoleIsBroadcaster = false; _resyncHeartbeatFromAgoraState(); if (_joinAgoraCompleterIsPending) { _joinAgoraCompleter?.completeError( StateError('Agora connection failed before join: $reason'), ); } else { unawaited( _handleAgoraDisconnected('Agora connection failed: $reason'), ); } return; } if (state == ConnectionStateType.connectionStateDisconnected || state == ConnectionStateType.connectionStateReconnecting) { _agoraJoined = false; _agoraRoleIsBroadcaster = false; _resyncHeartbeatFromAgoraState(); _scheduleAgoraDisconnectedCleanup(reason); } }, onAudioMixingStateChanged: ( AudioMixingStateType state, AudioMixingReasonType reason, ) { switch (state) { case AudioMixingStateType.audioMixingStatePlaying: isMusicPlaying = true; break; case AudioMixingStateType.audioMixingStateStopped: case AudioMixingStateType.audioMixingStateFailed: isMusicPlaying = false; break; default: break; } _roomMusicMixingStateListener?.call(state, reason); }, onUserJoined: (connection, remoteUid, elapsed) { debugPrint('rtc用户 $remoteUid 加入了频道'); }, // 监听远端用户离开 onUserOffline: (connection, remoteUid, reason) { debugPrint('rtc用户 $remoteUid 离开了频道 (原因: $reason)'); }, onTokenPrivilegeWillExpire: ( RtcConnection connection, String token, ) async { var rtcToken = await SCAccountRepository().getRtcToken( currenRoom?.roomProfile?.roomProfile?.id ?? "", AccountStorage().getCurrentUser()?.userProfile?.id ?? "", isPublisher: _agoraRoleIsBroadcaster, ); engine?.renewToken(rtcToken.rtcToken ?? ""); }, onAudioVolumeIndication: initializeAudioVolumeIndicationCallback, ); rtcEngine.registerEventHandler(_rtcEngineEventHandler!); await _configureAgoraAudioEngine(rtcEngine); return rtcEngine; } Future _configureAgoraAudioEngine(RtcEngine rtcEngine) async { if (_agoraAudioConfigured) { return; } await rtcEngine.setAudioProfile( profile: AudioProfileType.audioProfileSpeechStandard, scenario: AudioScenarioType.audioScenarioGameStreaming, ); await rtcEngine.enableAudioVolumeIndication( interval: 500, smooth: 3, reportVad: true, ); await rtcEngine.disableVideo(); _agoraAudioConfigured = true; } Future releaseRtcEngineForAppTermination() async { final rtcEngine = engine; if (rtcEngine == null) { return; } final eventHandler = _rtcEngineEventHandler; engine = null; _rtcEngineEventHandler = null; _rtcEngineInitTask = null; _rtcEnginePrewarmTask = null; _agoraAudioConfigured = false; try { if (eventHandler != null) { rtcEngine.unregisterEventHandler(eventHandler); } await rtcEngine.leaveChannel(); } catch (e) { debugPrint('rtc销毁前离开频道出错: $e'); } _resetAgoraTracking(); try { await rtcEngine.release(); } catch (e) { debugPrint('rtc释放引擎出错: $e'); } } 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; } if (!shouldShowSeatSoundWave(seatIndex)) { 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? roomBackground, 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), roomBackground: _preferNonEmpty( roomBackground, currentRoomProfile.roomBackground, ), roomName: _preferNonEmpty(roomName, currentRoomProfile.roomName), roomDesc: _preferNonEmpty(roomDesc, currentRoomProfile.roomDesc), ), ), ); SCRoomProfileCache.saveRoomProfile( roomId: currentRoomProfile.id ?? "", roomCover: roomCover, roomBackground: roomBackground, roomName: roomName, roomDesc: roomDesc, ); notifyListeners(); } void joinVoiceRoomSession( BuildContext context, String roomId, { String? pwd, bool clearRoomData = false, bool needOpenRedenvelope = false, String redPackId = "", int? previewSeatCount, RoomEntryPreviewData? previewData, }) 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) { if (_currentRoomIsEntryPreview || _roomStartupStatus == RoomStartupStatus.loading) { SCFloatIchart().remove(); if (!context.mounted) { return; } VoiceRoomRoute.openVoiceRoom(context); return; } ///最小化进入房间,或者进入的是同一个房间 final loaded = await loadRoomInfo( currenRoom?.roomProfile?.roomProfile?.id ?? "", ); if (!loaded) { return; } if (!context.mounted) { return; } Provider.of( context, listen: false, ).fetchUserProfileData(); retrieveMicrophoneList(); fetchOnlineUsersList(); _startRoomStatePolling(); _setRoomStartupReady(); setRoomVisualEffectsEnabled(true); SCFloatIchart().remove(); VoiceRoomRoute.openVoiceRoom(context); } else { SCFloatIchart().remove(); if (currenRoom != null) { _switchAwayFromCurrentRoomForNewEntry(); } if (!context.mounted) { return; } rtmProvider = Provider.of(context, listen: false); _clearRoomContributionLevelData(context); rtmProvider?.seedSystemRoomTips( SCAppLocalizations.of(context)!.systemRoomTips, ); _setRoomStartupLoading(); setRoomVisualEffectsEnabled(true); _applyRoomEntryPreview( roomId, previewData: previewData, previewSeatCount: previewSeatCount, ); VoiceRoomRoute.openVoiceRoom(context); notifyListeners(); final entryRequestSerial = ++_roomEntryRequestSerial; unawaited( _enterAndInitializeVoiceRoomSession( roomId, entryRequestSerial: entryRequestSerial, pwd: pwd, needOpenRedenvelope: needOpenRedenvelope, redPackId: redPackId, ), ); } } Future _enterAndInitializeVoiceRoomSession( String roomId, { required int entryRequestSerial, String? pwd, bool needOpenRedenvelope = false, String redPackId = "", }) async { try { final enteredRoom = await SCAccountRepository().entryRoom( roomId, pwd: pwd, redPackId: redPackId, needOpenRedenvelope: needOpenRedenvelope, ); if (!_isActiveRoomEntryRequest(entryRequestSerial)) { unawaited(_cleanupStaleEnteredRoom(enteredRoom)); return; } currenRoom = _mergeVoiceRoomVipHints(enteredRoom, currenRoom); _syncCurrentUserVipHintFromRoom(currenRoom); unawaited(_refreshCurrentUserVipHintForRoom(entryRequestSerial)); _currentRoomIsEntryPreview = false; _previewRoomSeatCount = null; notifyListeners(); await initializeRoomSession( entryRequestSerial: entryRequestSerial, needOpenRedenvelope: needOpenRedenvelope, redPackId: redPackId, shouldOpenRoomPage: false, ); } catch (e, stackTrace) { if (!_isActiveRoomEntryRequest(entryRequestSerial)) { return; } debugPrint('[RoomStartup] entry room failed: $e\n$stackTrace'); _setRoomStartupFailed(RoomStartupFailureType.entry); } } JoinRoomRes _mergeVoiceRoomVipHints( JoinRoomRes enteredRoom, JoinRoomRes? previewRoom, ) { final roomProfile = enteredRoom.roomProfile; if (roomProfile == null) { return enteredRoom; } final enteredOwner = roomProfile.userProfile; if (_profileHasPositiveVipLevelHint(enteredOwner)) { return enteredRoom; } final roomId = _firstNonBlankText([roomProfile.roomProfile?.id]); final ownerId = _firstNonBlankText([ roomProfile.roomProfile?.userId, enteredOwner?.id, ]); final previewRoomProfile = previewRoom?.roomProfile; final previewOwner = previewRoomProfile?.userProfile; final previewRoomId = _firstNonBlankText([ previewRoomProfile?.roomProfile?.id, ]); final previewOwnerId = _firstNonBlankText([ previewRoomProfile?.roomProfile?.userId, previewOwner?.id, ]); final currentProfile = AccountStorage().getCurrentUser()?.userProfile; SocialChatUserProfile? resolvedOwner = enteredOwner; String? vipLevel; final canUsePreviewOwner = previewOwner != null && _sameNonBlankId(roomId, previewRoomId) && (ownerId == null || _sameNonBlankId(ownerId, previewOwnerId)); if (canUsePreviewOwner) { vipLevel = _profileVipLevelHint(previewOwner); resolvedOwner ??= previewOwner; } if (vipLevel == null && _sameNonBlankId(ownerId, currentProfile?.id)) { vipLevel = _profileVipLevelHint(currentProfile) ?? _roomVipLevelHintFromEntrants(enteredRoom.entrants); resolvedOwner ??= currentProfile; } if (vipLevel == null || resolvedOwner == null) { return enteredRoom; } return enteredRoom.copyWith( roomProfile: roomProfile.copyWith( userProfile: resolvedOwner.copyWith(vipLevel: vipLevel), userSVipLevel: vipLevel, ), ); } void _syncCurrentUserVipHintFromRoom(JoinRoomRes? room) { final currentUser = AccountStorage().getCurrentUser(); final currentProfile = currentUser?.userProfile; if (currentUser == null || currentProfile == null || _profileHasPositiveVipLevelHint(currentProfile)) { return; } final ownerProfile = room?.roomProfile?.userProfile; final ownerId = _firstNonBlankText([ room?.roomProfile?.roomProfile?.userId, ownerProfile?.id, ]); String? vipLevel; if (_sameNonBlankId(ownerId, currentProfile.id)) { vipLevel = _profileVipLevelHint(ownerProfile); } vipLevel ??= _roomVipLevelHintFromEntrants(room?.entrants); if (vipLevel == null) { return; } AccountStorage().setCurrentUser( currentUser.copyWith( userProfile: currentProfile.copyWith(vipLevel: vipLevel), ), ); } Future _refreshCurrentUserVipHintForRoom(int entryRequestSerial) async { final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? ""; if (roomId.isEmpty) { return; } final currentUser = AccountStorage().getCurrentUser(); final currentProfile = currentUser?.userProfile; if (currentUser == null || currentProfile == null) { return; } final existingVipLevel = _positiveProfileVipLevelHint(currentProfile); final vipLevel = existingVipLevel ?? await _loadCurrentVipLevelForColoredId(); if (vipLevel == null || vipLevel.isEmpty) { return; } final updatedProfile = currentProfile.copyWith(vipLevel: vipLevel); if (!_profileHasPositiveVipLevelHint(currentProfile)) { AccountStorage().setCurrentUser( currentUser.copyWith(userProfile: updatedProfile), ); } if (!_isCurrentRoomEntrySession(entryRequestSerial, roomId)) { return; } var changed = false; final mergedRoom = _mergeKnownVipIntoCurrentRoom( currenRoom, updatedProfile, ); if (!identical(mergedRoom, currenRoom)) { currenRoom = mergedRoom; changed = true; } final mergedOnlineUsers = onlineUsers .map((user) => _mergeKnownVipIntoProfile(user) ?? user) .toList(); if (!_sameOnlineUsers(onlineUsers, mergedOnlineUsers)) { onlineUsers = mergedOnlineUsers; _refreshManagerUsers(onlineUsers); changed = true; } final mergedMicMap = roomWheatMap.map( (index, mic) => MapEntry(index, _mergeKnownVipIntoMic(mic)), ); if (!_sameMicMaps(roomWheatMap, mergedMicMap)) { roomWheatMap = mergedMicMap; changed = true; } if (changed) { notifyListeners(); } } Future _loadCurrentVipLevelForColoredId() async { final repository = SCVipRepositoryImp(); try { final home = await repository.vipHome(); final homeVipLevel = _vipLevelFromStatus(home.state); if (homeVipLevel != null) { return homeVipLevel; } } catch (_) {} try { return _vipLevelFromStatus(await repository.vipStatus()); } catch (_) { return null; } } String? _vipLevelFromStatus(SCVipStatusRes? status) { if (status == null) { return null; } if (status.active == false) { return '0'; } final level = status.levelInt; return level > 0 ? level.toString() : '0'; } JoinRoomRes? _mergeKnownVipIntoCurrentRoom( JoinRoomRes? room, SocialChatUserProfile currentProfile, ) { final roomProfile = room?.roomProfile; final ownerProfile = roomProfile?.userProfile; if (room == null || roomProfile == null || ownerProfile == null) { return room; } final ownerId = _firstNonBlankText([ roomProfile.roomProfile?.userId, ownerProfile.id, ]); if (!_sameNonBlankId(ownerId, currentProfile.id) || _profileHasPositiveVipLevelHint(ownerProfile)) { return room; } final vipLevel = _positiveProfileVipLevelHint(currentProfile); if (vipLevel == null) { return room; } return room.copyWith( roomProfile: roomProfile.copyWith( userProfile: ownerProfile.copyWith(vipLevel: vipLevel), userSVipLevel: vipLevel, ), ); } MicRes _mergeKnownVipIntoMic(MicRes mic) { final mergedUser = _mergeKnownVipIntoProfile(mic.user); if (identical(mergedUser, mic.user)) { return mic; } return mic.copyWith(user: mergedUser); } SocialChatUserProfile? _mergeKnownVipIntoProfile( SocialChatUserProfile? profile, ) { if (profile == null || _profileHasPositiveVipLevelHint(profile)) { return profile; } final currentProfile = AccountStorage().getCurrentUser()?.userProfile; final currentVipLevel = _positiveProfileVipLevelHint(currentProfile); if (_sameNonBlankId(profile.id, currentProfile?.id) && currentVipLevel != null) { return profile.copyWith(vipLevel: currentVipLevel); } final ownerProfile = currenRoom?.roomProfile?.userProfile; final ownerVipLevel = _positiveProfileVipLevelHint(ownerProfile); final ownerId = _firstNonBlankText([ currenRoom?.roomProfile?.roomProfile?.userId, ownerProfile?.id, ]); if (_sameNonBlankId(profile.id, ownerId) && ownerVipLevel != null) { return profile.copyWith(vipLevel: ownerVipLevel); } return profile; } bool _profileHasPositiveVipLevelHint(SocialChatUserProfile? profile) { return _positiveProfileVipLevelHint(profile) != null; } String? _profileVipLevelHint(SocialChatUserProfile? profile) { return _firstNonBlankText([profile?.vipLevel]); } String? _positiveProfileVipLevelHint(SocialChatUserProfile? profile) { final vipLevel = _profileVipLevelHint(profile); if (profile == null || profile.vipLevelForColoredId <= 0) { return null; } return vipLevel ?? profile.vipLevelForColoredId.toString(); } String? _roomVipLevelHintFromEntrants(Entrants? entrants) { final abilities = entrants?.nobleVipAbility ?? const []; var hasAbility = false; for (final ability in abilities) { final text = ability.trim(); final normalized = text.toLowerCase(); if (text.isEmpty || normalized == '0' || normalized == 'false') { continue; } hasAbility = true; if (_looksLikeVipLevelText(text)) { return text; } } return hasAbility ? '1' : null; } bool _looksLikeVipLevelText(String text) { final plainLevel = int.tryParse(text); if ((plainLevel ?? 0) > 0) { return true; } if (RegExp( r'(?:S?VIP|NOBLE[_\s-]*VIP)[^\d]*[1-9]\d*', caseSensitive: false, ).hasMatch(text)) { return true; } return RegExp(r'[1-9]\d*$').hasMatch(text); } String? _firstNonBlankText(Iterable values) { for (final value in values) { final text = value?.toString().trim(); if (text != null && text.isNotEmpty) { return text; } } return null; } bool _sameNonBlankId(dynamic left, dynamic right) { final leftText = left?.toString().trim(); final rightText = right?.toString().trim(); return leftText != null && leftText.isNotEmpty && rightText != null && rightText.isNotEmpty && leftText == rightText; } bool _isActiveRoomEntryRequest(int entryRequestSerial) { return entryRequestSerial == _roomEntryRequestSerial && _roomStartupStatus == RoomStartupStatus.loading; } bool _isCurrentRoomEntrySession(int entryRequestSerial, String roomId) { return entryRequestSerial == _roomEntryRequestSerial && roomId.isNotEmpty && currenRoom?.roomProfile?.roomProfile?.id == roomId; } bool _isActiveRoomStartup(int entryRequestSerial, String roomId) { return _isActiveRoomEntryRequest(entryRequestSerial) && _isCurrentRoomEntrySession(entryRequestSerial, roomId); } void _setPreviewRoomSeatCount(int? seatCount, {bool notify = true}) { final normalizedSeatCount = _normalizePreviewRoomSeatCount(seatCount); if (_previewRoomSeatCount == normalizedSeatCount) { return; } _previewRoomSeatCount = normalizedSeatCount; if (notify) { notifyListeners(); } } int? _normalizePreviewRoomSeatCount(int? seatCount) { switch (seatCount) { case 5: case 10: case 15: case 20: return seatCount; default: return null; } } void _applyRoomEntryPreview( String roomId, { RoomEntryPreviewData? previewData, int? previewSeatCount, }) { final resolvedPreviewSeatCount = previewData?.seatCount ?? previewSeatCount; _setPreviewRoomSeatCount(resolvedPreviewSeatCount, notify: false); if (previewData == null) { return; } currenRoom = previewData.toJoinRoomRes(fallbackRoomId: roomId); _currentRoomIsEntryPreview = true; } Future _cleanupStaleEnteredRoom(JoinRoomRes enteredRoom) async { final staleRoomId = enteredRoom.roomProfile?.roomProfile?.id ?? ""; if (staleRoomId.isEmpty) { return; } if (_isCurrentRoomId(staleRoomId)) { debugPrint( '[RoomStartup] skip stale entry cleanup: room $staleRoomId is active again', ); return; } await _retryExitNetworkRequest('quit stale entered room $staleRoomId', () { return SCAccountRepository().quitRoom(staleRoomId).then((didQuit) { if (!didQuit) { throw StateError('quitRoom returned false'); } }); }); } ///初始化房间相关 Future initializeRoomSession({ required int entryRequestSerial, bool needOpenRedenvelope = false, String? redPackId, bool shouldOpenRoomPage = true, }) async { _disableMicListRefreshForCurrentSession = false; _disableOnlineUsersRefreshForCurrentSession = false; final startupRoomId = currenRoom?.roomProfile?.roomProfile?.id ?? ""; if (!_isActiveRoomStartup(entryRequestSerial, startupRoomId)) { return; } if ((currenRoom?.roomProfile?.roomProfile?.event == SCRoomInfoEventType.WAITING_CONFIRMED.name || currenRoom?.roomProfile?.roomProfile?.event == SCRoomInfoEventType.ID_CHANGE.name) && currenRoom?.entrants?.roles == SCRoomRolesType.HOMEOWNER.name) { ///需要去修改房间信息,创建群聊 VoiceRoomRoute.openRoomEdit(context!, needRestCurrentRoomInfo: true); return; } SCRoomUtils.roomSCGlobalConfig( currenRoom?.roomProfile?.roomProfile?.id ?? "", ); if (currenRoom?.roomProfile?.roomProfile?.id != AccountStorage().getCurrentUser()?.userProfile?.id) { final followRoomId = startupRoomId; SCChatRoomRepository().isFollowRoom(followRoomId).then((value) { if (!_isActiveRoomStartup(entryRequestSerial, followRoomId)) { return; } isFollowRoomRes = value; notifyListeners(); }); } _setRoomStartupLoading(); setRoomVisualEffectsEnabled(true); if (shouldOpenRoomPage) { VoiceRoomRoute.openVoiceRoom(context!); } unawaited( _bootstrapEnteredVoiceRoomSession( entryRequestSerial: entryRequestSerial, roomId: startupRoomId, groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "", ), ); } Future _bootstrapEnteredVoiceRoomSession({ required int entryRequestSerial, required String roomId, required String groupId, }) async { var failureType = RoomStartupFailureType.im; if (!_isActiveRoomStartup(entryRequestSerial, roomId)) { return; } try { final joinGroupFuture = rtmProvider?.joinRoomGroup(groupId, ""); final rtcTokenFuture = _captureStartupFuture( _fetchRtcTokenForCurrentRoom(isPublisher: false), ); final microphoneListFuture = retrieveMicrophoneList(); final onlineUsersFuture = fetchOnlineUsersList(); final joinResult = await joinGroupFuture; if (!_isActiveRoomStartup(entryRequestSerial, roomId)) { return; } if (joinResult == null || joinResult.code != 0) { debugPrint( '[RoomStartup] join IM group failed code=${joinResult?.code} desc=${joinResult?.desc}', ); _setRoomStartupFailed(RoomStartupFailureType.im); return; } rtmProvider?.seedSystemRoomTips( SCAppLocalizations.of(context!)!.systemRoomTips, groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "", ); rtmProvider?.addMsg( Msg( groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "", msg: currenRoom?.roomProfile?.roomProfile?.roomDesc, type: SCRoomMsgType.systemTips, ), ); await Future.wait([microphoneListFuture, onlineUsersFuture]); if (!_isActiveRoomStartup(entryRequestSerial, roomId)) { return; } _startRoomStatePolling(); final currentContext = context; if (currentContext != null && currentContext.mounted) { Provider.of( currentContext, listen: false, ).fetchContributionLevelData(roomId); } failureType = RoomStartupFailureType.rtc; final rtcTokenResult = await rtcTokenFuture; if (!_isActiveRoomStartup(entryRequestSerial, roomId)) { return; } final rtcToken = rtcTokenResult.requireValue(); await joinAgoraVoiceChannel(throwOnError: true, rtcToken: rtcToken); if (!_isActiveRoomStartup(entryRequestSerial, roomId)) { await _cleanupAgoraRoomState( clearMicSeats: currenRoom == null, onlyIfJoinedChannelId: roomId, ); return; } await _bootstrapRoomHeartbeatState(); if (!_isActiveRoomStartup(entryRequestSerial, roomId)) { return; } _setRoomStartupReady(); 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); _playRoomEntryEffects( joinMsg, entryRequestSerial: entryRequestSerial, roomId: roomId, ); _loadRoomSecondaryData(); } catch (error, stackTrace) { if (!_isActiveRoomStartup(entryRequestSerial, roomId)) { debugPrint('[RoomStartup] ignore canceled bootstrap failure: $error'); return; } debugPrint('[RoomStartup] bootstrap failed: $error\n$stackTrace'); _setRoomStartupFailed(failureType); } } void _playRoomEntryEffects( Msg joinMsg, { required int entryRequestSerial, required String roomId, }) { if (SCGlobalConfig.isEntryVehicleAnimation) { final sourceUrl = AccountStorage() .getCurrentUser() ?.userProfile ?.getMountains() ?.sourceUrl ?? ""; if (sourceUrl.isNotEmpty && shouldShowRoomVisualEffects && _isCurrentRoomEntrySession(entryRequestSerial, roomId)) { _roomEntryEffectTimer?.cancel(); _roomEntryEffectTimer = Timer(const Duration(milliseconds: 550), () { _roomEntryEffectTimer = null; if (!shouldShowRoomVisualEffects || !_isCurrentRoomEntrySession(entryRequestSerial, roomId)) { return; } SCGiftVapSvgaManager().play( sourceUrl, priority: SCGiftVapSvgaManager.entryEffectPriority, type: SCGiftVapSvgaManager.entryEffectType, ); }); } } if (rtmProvider?.msgUserJoinListener != null) { rtmProvider?.msgUserJoinListener!(joinMsg); } } void _loadRoomSecondaryData() { SCChatRoomRepository() .rocketClaim(currenRoom?.roomProfile?.roomProfile?.id ?? "") .catchError((e) { return true; // 错误已处理 }); 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 || _disableOnlineUsersRefreshForCurrentSession) { return; } _isRefreshingOnlineUsers = true; try { final fetchedUsers = await SCChatRoomRepository().roomOnlineUsers( roomId, silentErrorToast: true, ); if (roomId != currenRoom?.roomProfile?.roomProfile?.id) { return; } final mergedUsers = fetchedUsers.map((user) => _mergeKnownVipIntoProfile(user)!).toList(); final changed = !_sameOnlineUsers(onlineUsers, mergedUsers); onlineUsers = mergedUsers; _refreshManagerUsers(onlineUsers); if (changed || notifyIfUnchanged) { notifyListeners(); } } catch (error) { if (_isMissingPathError(error)) { _disableOnlineUsersRefreshForCurrentSession = true; debugPrint( '[Room][OnlineUsers] suspend auto refresh for current session: ${_errorMessageFrom(error)}', ); } else { debugPrint( '[Room][OnlineUsers] refresh failed: ${_errorMessageFrom(error)}', ); } } finally { _isRefreshingOnlineUsers = false; } } Future retrieveMicrophoneList({bool notifyIfUnchanged = true}) async { final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? ""; if (roomId.isEmpty || _disableMicListRefreshForCurrentSession) { return; } if (_isRefreshingMicList) { _pendingMicListRefresh = true; _pendingMicListRefreshNotifyIfUnchanged = _pendingMicListRefreshNotifyIfUnchanged || notifyIfUnchanged; return; } _isRefreshingMicList = true; _lastMicListRefreshStartedAtMs = DateTime.now().millisecondsSinceEpoch; try { final roomWheatList = await SCChatRoomRepository().micList( roomId, silentErrorToast: true, ); if (roomId != currenRoom?.roomProfile?.roomProfile?.id) { return; } final nextMap = _buildMicMap(roomWheatList); _applyMicSnapshot(nextMap, notifyIfUnchanged: notifyIfUnchanged); } catch (error) { if (_isMissingPathError(error)) { _disableMicListRefreshForCurrentSession = true; _deferredMicListRefreshTimer?.cancel(); _deferredMicListRefreshTimer = null; debugPrint( '[Room][MicList] suspend auto refresh for current session: ${_errorMessageFrom(error)}', ); if (notifyIfUnchanged) { notifyListeners(); } } else { debugPrint( '[Room][MicList] refresh failed: ${_errorMessageFrom(error)}', ); } } finally { _isRefreshingMicList = false; if (_pendingMicListRefresh) { _pendingMicListRefresh = false; final pendingNotifyIfUnchanged = _pendingMicListRefreshNotifyIfUnchanged; _pendingMicListRefreshNotifyIfUnchanged = false; unawaited( Future.microtask( () => retrieveMicrophoneList( notifyIfUnchanged: notifyIfUnchanged || pendingNotifyIfUnchanged, ), ), ); } } } 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, { bool deferRemoteCleanup = true, }) async { if (_isExitingCurrentVoiceRoomSession) { return; } _setExitingCurrentVoiceRoomSession(true); _stopRoomStatePolling(); final groupId = currenRoom?.roomProfile?.roomProfile?.roomAccount ?? ""; final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? ""; final isPreviewOnlyExit = _currentRoomIsEntryPreview; final exitRtmProvider = rtmProvider; final navigationContext = context; try { exitRtmProvider?.msgAllListener = null; exitRtmProvider?.msgChatListener = null; exitRtmProvider?.msgGiftListener = null; SCGiftVapSvgaManager().stopPlayback(); setRoomVisualEffectsEnabled(false); SCHeartbeatUtils.cancelTimer(); exitRtmProvider?.cleanRoomData(); } catch (e) { debugPrint('rtc退出房间本地清理出错: $e'); } _clearData(); if (!isLogout && navigationContext != null) { SCNavigatorUtils.popUntil( navigationContext, ModalRoute.withName(SCRoutes.home), ); } final agoraLeaveTask = _leaveAgoraRoomForExit(); final remoteCleanupTask = _cleanupExitedRoomRemotely( exitRtmProvider: exitRtmProvider, groupId: isPreviewOnlyExit ? "" : groupId, roomId: isPreviewOnlyExit ? "" : roomId, shouldSendOnlineHeartbeat: !isLogout, ); if (deferRemoteCleanup) { unawaited(agoraLeaveTask); unawaited(remoteCleanupTask); } else { await agoraLeaveTask; await remoteCleanupTask; } } void _switchAwayFromCurrentRoomForNewEntry() { _stopRoomStatePolling(); final groupId = currenRoom?.roomProfile?.roomProfile?.roomAccount ?? ""; final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? ""; final isPreviewOnlyExit = _currentRoomIsEntryPreview; final exitRtmProvider = rtmProvider; try { exitRtmProvider?.msgAllListener = null; exitRtmProvider?.msgChatListener = null; exitRtmProvider?.msgGiftListener = null; SCGiftVapSvgaManager().stopPlayback(); setRoomVisualEffectsEnabled(false); SCHeartbeatUtils.cancelTimer(); exitRtmProvider?.cleanRoomData(); } catch (e) { debugPrint('rtc切换房间本地清理出错: $e'); } _clearData(); final agoraLeaveTask = _leaveAgoraRoomForExit(); _pendingRoomSwitchAgoraLeaveTask = agoraLeaveTask; unawaited( agoraLeaveTask.whenComplete(() { if (identical(_pendingRoomSwitchAgoraLeaveTask, agoraLeaveTask)) { _pendingRoomSwitchAgoraLeaveTask = null; } }), ); unawaited( _cleanupExitedRoomRemotely( exitRtmProvider: exitRtmProvider, groupId: isPreviewOnlyExit ? "" : groupId, roomId: isPreviewOnlyExit ? "" : roomId, shouldSendOnlineHeartbeat: false, ), ); } Future _cleanupExitedRoomRemotely({ required RtmProvider? exitRtmProvider, required String groupId, required String roomId, required bool shouldSendOnlineHeartbeat, }) async { final tasks = >[]; if (shouldSendOnlineHeartbeat) { tasks.add( _retryExitNetworkRequest('heartbeat ONLINE', () async { if (currenRoom != null) { debugPrint('[RoomExit] skip ONLINE heartbeat: already in a room'); return; } await SCHeartbeatUtils.scheduleHeartbeat( SCHeartbeatStatus.ONLINE.name, false, rethrowOnError: true, ); }), ); } if (groupId.isNotEmpty && exitRtmProvider != null) { tasks.add( _retryExitNetworkRequest('quit IM group $groupId', () async { if (_isCurrentRoomGroup(groupId)) { debugPrint('[RoomExit] skip quitGroup: re-entered group $groupId'); return; } await exitRtmProvider.quitGroup(groupId); }), ); } if (roomId.isNotEmpty) { tasks.add( _retryExitNetworkRequest('quit room $roomId', () async { if (_isCurrentRoomId(roomId)) { debugPrint('[RoomExit] skip quitRoom: re-entered room $roomId'); return; } final didQuit = await SCAccountRepository().quitRoom(roomId); if (!didQuit) { throw StateError('quitRoom returned false'); } }), ); } await Future.wait(tasks); } Future _retryExitNetworkRequest( String name, Future Function() action, ) async { Object? lastError; StackTrace? lastStackTrace; for (var attempt = 1; attempt <= _exitNetworkRetryLimit; attempt++) { try { await action(); return; } catch (error, stackTrace) { lastError = error; lastStackTrace = stackTrace; debugPrint( '[RoomExit] $name failed attempt ' '$attempt/$_exitNetworkRetryLimit: $error', ); if (attempt < _exitNetworkRetryLimit) { await Future.delayed(_exitNetworkRetryDelay * attempt); } } } debugPrint( '[RoomExit] $name failed after $_exitNetworkRetryLimit attempts, ' 'treat as backend/remote issue: $lastError\n$lastStackTrace', ); } bool _isCurrentRoomId(String roomId) { return roomId.isNotEmpty && currenRoom?.roomProfile?.roomProfile?.id == roomId; } bool _isCurrentRoomGroup(String groupId) { return groupId.isNotEmpty && currenRoom?.roomProfile?.roomProfile?.roomAccount == groupId; } 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 _cleanupAgoraRoomState(); await Future.delayed(const Duration(milliseconds: 100)); } catch (e) { debugPrint('rtc清理本地房间状态出错: $e'); } finally { rtmProvider?.cleanRoomData(); _clearData(); notifyListeners(); } } Future confirmRoomStartupFailureAndLeave({ BuildContext? navigationContext, }) async { if (_isHandlingRoomStartupFailure) { return; } _isHandlingRoomStartupFailure = true; final failedRoom = currenRoom; final isPreviewOnlyFailure = _currentRoomIsEntryPreview; final groupId = failedRoom?.roomProfile?.roomProfile?.roomAccount ?? ""; final roomId = failedRoom?.roomProfile?.roomProfile?.id ?? ""; final navigatorContext = navigationContext ?? context; final navigator = navigatorContext != null ? Navigator.maybeOf(navigatorContext) : null; try { _stopRoomStatePolling(); SCGiftVapSvgaManager().stopPlayback(); SCHeartbeatUtils.cancelTimer(); SCHeartbeatUtils.cancelAnchorTimer(); rtmProvider?.cleanRoomData(); if (!isPreviewOnlyFailure && groupId.isNotEmpty) { await rtmProvider?.quitGroup(groupId); } await _cleanupAgoraRoomState(clearMicSeats: true); await Future.delayed(const Duration(milliseconds: 100)); if (!isPreviewOnlyFailure && roomId.isNotEmpty) { await SCAccountRepository().quitRoom(roomId); } } catch (e) { debugPrint('rtc进房失败清理本地房间状态出错: $e'); } finally { _clearData(); _isHandlingRoomStartupFailure = false; notifyListeners(); if (navigator != null && navigator.mounted && navigator.canPop()) { navigator.pop(); } } } ///清空列表数据 void _clearData() { _roomEntryRequestSerial += 1; _roomEntryEffectTimer?.cancel(); _roomEntryEffectTimer = null; _previewRoomSeatCount = null; _finishRoomStartupSeatLoading(executePendingAction: false); _resetSelfMicGoUpFailureState(); _clearRoomContributionLevelData(); _stopRoomStatePolling(); _resetHeartbeatTracking(); _resetAgoraTracking(); _disableMicListRefreshForCurrentSession = false; _disableOnlineUsersRefreshForCurrentSession = false; _isSelfMicActionInFlight = false; _roomStartupStatus = RoomStartupStatus.idle; _roomStartupFailureType = RoomStartupFailureType.none; _currentRoomIsEntryPreview = false; _isHandlingRoomStartupFailure = false; _isHandlingAgoraRoomFailure = false; _pendingMicListRefresh = false; _pendingMicListRefreshNotifyIfUnchanged = false; _seatEmojiEventVersions.clear(); _emojiPlaybackEventVersion = 0; 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 _clearRoomContributionLevelData([BuildContext? targetContext]) { final currentContext = targetContext ?? context; if (currentContext == null || !currentContext.mounted) { return; } Provider.of( currentContext, listen: false, ).clearContributionLevelData(); } @override void dispose() { _stopRoomStatePolling(); unawaited(releaseRtcEngineForAppTermination()); super.dispose(); } void toggleRemoteAudioMuteForAllUsers() { roomIsMute = !roomIsMute; engine?.muteAllRemoteAudioStreams(roomIsMute); notifyListeners(); } ///点击的位置 void clickSite(num index, {SocialChatUserProfile? clickUser}) { if (_shouldDeferSeatInteractionForRoomStartup) { _deferRoomStartupSeatAction( _PendingRoomStartupSeatAction.clickSeat( index: index, clickUser: clickUser, ), ); return; } 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 = micAtIndexForDisplay(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 = micAtIndexForDisplay(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({bool notifyIfUnchanged = false}) { retrieveMicrophoneList( notifyIfUnchanged: notifyIfUnchanged, ).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); debugPrint('添加监听:${_onSoundVoiceChangeList.length}'); } removeSoundVoiceChangeListener(OnSoundVoiceChange onSoundVoiceChange) { _onSoundVoiceChangeList.remove(onSoundVoiceChange); debugPrint('删除监听:${_onSoundVoiceChangeList.length}'); } void shangMai(num index, {String? eventType, String? inviterId}) async { if (_shouldDeferSeatInteractionForRoomStartup) { _deferRoomStartupSeatAction( _PendingRoomStartupSeatAction.goUpMic( index: index, eventType: eventType, inviterId: inviterId, ), ); return; } var myUser = AccountStorage().getCurrentUser()?.userProfile; if (currenRoom?.roomProfile?.roomSetting?.touristMike ?? false) { } else { if (isTourists()) { SCTts.show( SCAppLocalizations.of(context!)!.touristsAreNotAllowedToGoOnTheMic, ); return; } } final shouldShowRetryLoading = _selfMicGoUpConsecutiveFailureCount > 0; if (_isSelfMicActionInFlight) { if (shouldShowRetryLoading) { _showSelfMicGoUpRetryLoading(); } return; } if (shouldShowRetryLoading) { _showSelfMicGoUpRetryLoading(); } _isSelfMicActionInFlight = true; try { var micGoUpRes = await SCChatRoomRepository().micGoUp( currenRoom?.roomProfile?.roomProfile?.id ?? "", index, eventType: eventType, inviterId: inviterId, ); final targetIndex = micGoUpRes.micIndex ?? index; final switched = await _switchAgoraToBroadcaster( publisherToken: micGoUpRes.roomToken ?? "", muted: (micGoUpRes.micMute ?? false) || isMic, ); if (!switched) { await _switchAgoraToAudience(); try { await SCChatRoomRepository().micGoDown( currenRoom?.roomProfile?.roomProfile?.id ?? "", targetIndex, ); } catch (downError) { debugPrint( '[Agora] release backend mic after broadcaster switch failed: $downError', ); } _clearSelfMicSwitchGuard(clearPreferredIndex: true); _clearUserFromSeats(myUser?.id); _syncSelfMicRuntimeState(); notifyListeners(); _refreshMicListSilently(notifyIfUnchanged: true); _handleSelfMicGoUpFailure( 'Failed to put on the microphone, Agora switch failed', ); return; } 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, ); } _syncSelfMicRuntimeState(); if (roomWheatMap[targetIndex]?.micMute ?? false) { ///房主上麦自动解禁麦位 if (isFz()) { await jieJinMai(targetIndex); } } notifyListeners(); _refreshMicListSilently(notifyIfUnchanged: true); _resetSelfMicGoUpFailureState(); } catch (ex) { if (_isCancelledRequest(ex)) { _refreshMicListSilently(notifyIfUnchanged: true); return; } _handleSelfMicGoUpFailure( 'Failed to put on the microphone, ${_errorMessageFrom(ex)}', ); } finally { _dismissSelfMicGoUpRetryLoading(); _isSelfMicActionInFlight = false; } } xiaMai(num index) async { if (_isSelfMicActionInFlight) { return; } _isSelfMicActionInFlight = true; 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; } _clearSelfMicSwitchGuard(clearPreferredIndex: true); if (currentUserSeatIndex > -1) { _startSelfMicReleaseGuard(sourceIndex: currentUserSeatIndex); } else { _clearSelfMicReleaseGuard(); } SCHeartbeatUtils.cancelAnchorTimer(); await _switchAgoraToAudience(); _clearUserFromSeats(currentUserId); _syncSelfMicRuntimeState(); notifyListeners(); requestMicrophoneListRefresh( notifyIfUnchanged: false, minInterval: const Duration(milliseconds: 350), ); } catch (ex) { if (_isCancelledRequest(ex)) { _refreshMicListSilently(notifyIfUnchanged: true); return; } SCTts.show('Failed to leave the microphone, ${_errorMessageFrom(ex)}'); } finally { _isSelfMicActionInFlight = false; } } ///踢人下麦 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"); debugPrint('踢人下麦失败'); } } ///自己是否在麦上 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 ?? ""} roomBackground=${value.roomBackground ?? ""} 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, ), roomBackground: _preferNonEmpty( value.roomBackground, currentRoomProfile.roomBackground, ), 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 ?? ""} roomBackground=${currenRoom?.roomProfile?.roomProfile?.roomBackground ?? ""}", ); ///如果是游客禁止上麦,已经在麦上的游客需要下麦 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)) { await _switchAgoraToBroadcaster( publisherToken: roomWheatMap[index]?.roomToken ?? "", muted: 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 (!isMic) { if (roomWheatMap[index]?.user != null) { await _switchAgoraToBroadcaster( publisherToken: roomWheatMap[index]?.roomToken ?? "", muted: 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; } final mergedUser = _mergeKnownVipIntoProfile(user) ?? user; bool isExtOnlineList = false; for (var us in onlineUsers) { if (us.id == mergedUser.id) { isExtOnlineList = true; break; } } if (!isExtOnlineList) { Provider.of( context!, listen: false, ).onlineUsers.add(mergedUser); _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) { final seatIndex = msg.number; if (seatIndex == null || seatIndex < 0) { return; } var mic = roomWheatMap[seatIndex]; if (mic != null && mic.user != null) { _emojiPlaybackEventVersion += 1; _seatEmojiEventVersions[seatIndex] = _emojiPlaybackEventVersion; roomWheatMap[seatIndex] = 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); } void setRoomMusicMixingStateListener(RoomMusicMixingStateListener? listener) { _roomMusicMixingStateListener = listener; } void setRoomMusicMicRouteChangedListener( RoomMusicMicRouteChangedListener? listener, ) { _roomMusicMicRouteChangedListener = listener; } Future startRoomMusicAudioMixing({ required String filePath, required bool loopback, int cycle = 1, int startPos = 0, }) async { if (filePath.trim().isEmpty) { return; } await engine?.startAudioMixing( filePath: filePath, loopback: loopback, cycle: cycle, startPos: startPos, ); } Future pauseRoomMusicAudioMixing() async { await engine?.pauseAudioMixing(); } Future resumeRoomMusicAudioMixing() async { await engine?.resumeAudioMixing(); } Future stopRoomMusicAudioMixing() async { await engine?.stopAudioMixing(); } Future setRoomMusicAudioMixingPosition(int positionMs) async { await engine?.setAudioMixingPosition(positionMs); } Future getRoomMusicAudioMixingPosition() async { return await engine?.getAudioMixingCurrentPosition() ?? 0; } Future getRoomMusicAudioMixingDuration() async { return await engine?.getAudioMixingDuration() ?? 0; } Future setRoomMusicAudioMixingVolume(int volume) async { final safeVolume = volume.clamp(0, 100).toInt(); await engine?.adjustAudioMixingPlayoutVolume(safeVolume); await engine?.adjustAudioMixingPublishVolume(safeVolume); } }