import 'dart:async'; import 'dart:math' as math; import 'package:dio/dio.dart'; import 'package:flutter/foundation.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/tools/sc_room_top_layer_guard.dart'; import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart'; import 'package:yumi/shared/data_sources/models/message/room_rocket_launch_broadcast_message.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/room_rtc_engine_adapter.dart'; import 'package:yumi/services/audio/room_rtc_types.dart'; import 'package:yumi/services/audio/rtm_manager.dart'; import 'package:yumi/services/audio/trtc_room_rtc_engine_adapter.dart'; import 'package:yumi/services/audio/voice_room_foreground_service.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/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' hide PropsResources; 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' hide PropsResources; 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 'package:yumi/ui_kit/widgets/room/room_password_dialog.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_rocket_api_res.dart'; import '../../shared/business_logic/models/res/sc_room_red_packet_config_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'; import '../../ui_kit/widgets/room/rocket/room_rocket_asset_preloader.dart'; import '../../ui_kit/widgets/room/rocket/room_rocket_pag_effect_overlay.dart'; import '../../ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart'; typedef OnSoundVoiceChange = Function(num index, int volum); typedef RoomMusicSessionExitListener = Future Function(); typedef RtcProvider = RealTimeCommunicationManager; enum RoomStartupStatus { idle, loading, ready, failed } enum RoomStartupFailureType { none, entry, kicked, im, rtc } enum _RoomRtcLocalAudioRole { audience, anchor } 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; } } 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.existsPassword, 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, existsPassword: room.extValues?.existsPassword == true, 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, existsPassword: roomProfile?.extValues?.existsPassword == true, 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, existsPassword: (room.setting?.password ?? "").trim().isNotEmpty, 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 bool? existsPassword; 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 _PersistedVoiceRoomCleanupRequest { const _PersistedVoiceRoomCleanupRequest({ required this.staleRoomId, required this.targetRoomId, required this.restoreOnlineHeartbeatWhenIdle, this.clearMarkerOnly = false, }); final String staleRoomId; final String targetRoomId; final bool restoreOnlineHeartbeatWhenIdle; final bool clearMarkerOnly; } class _RoomMicRelationProfileCache { const _RoomMicRelationProfileCache({ required this.profile, required this.loadedAt, }); final SocialChatUserProfile profile; final DateTime loadedAt; bool isExpired(DateTime now, Duration ttl) => now.difference(loadedAt) > ttl; } class _RecentMicChangeSeatSnapshot { const _RecentMicChangeSeatSnapshot({ required this.mic, required this.expiresAtMs, }); final MicRes mic; final int expiresAtMs; String get userId => (mic.user?.id ?? "").trim(); bool isExpired(int nowMs) => expiresAtMs <= nowMs; } class RealTimeCommunicationManager extends ChangeNotifier { static const String _roomRocketPagLaunchDialogTag = 'showRoomRocketPagLaunchDialog'; static const List _roomRocketGiftRefreshDelays = [ Duration(milliseconds: 700), Duration(milliseconds: 1600), Duration(milliseconds: 3200), ]; static const List _roomRocketPostLaunchRefreshDelays = [ Duration(milliseconds: 800), Duration(seconds: 2), Duration(seconds: 5), Duration(seconds: 10), ]; static const List _roomRocketEntryReadyFallbackDelays = [ Duration(milliseconds: 300), Duration(milliseconds: 900), Duration(milliseconds: 1800), Duration(milliseconds: 3200), ]; static const List _roomMicRelationTypes = [ 'CP', 'BROTHER', 'SISTERS', ]; static const Duration _roomMicRelationCacheTtl = Duration(minutes: 5); static const Duration _roomMicRelationDismissedPairTtl = Duration(minutes: 5); static const int _roomPasswordNotTrueErrorCode = 9005; static const Duration _micListPollingInterval = Duration(seconds: 2); static const Duration _onlineUsersPollingInterval = Duration(seconds: 3); static const Duration _micChangeSnapshotGracePeriod = Duration(seconds: 4); static const int _exitNetworkRetryLimit = 3; static const Duration _exitNetworkRetryDelay = Duration(milliseconds: 500); static const Duration _selfMicStateGracePeriod = Duration(seconds: 4); static const Duration _seatResizeOccupantPreservePeriod = Duration( seconds: 8, ); static const Duration _giftTriggeredMicRefreshMinInterval = Duration( milliseconds: 900, ); static const Duration _roomMusicPublishingStateTtl = Duration(seconds: 15); static const String _roomStartupSeatLoadingTag = 'roomStartupSeatInteractionLoading'; static const String _selfMicGoUpRetryLoadingTag = 'selfMicGoUpRetryLoading'; static const int _selfMicGoUpFailureLimit = 3; static const List _roomRedPacketEntryRefreshDelays = [ Duration.zero, Duration(seconds: 2), Duration(seconds: 6), ]; bool needUpDataUserInfo = false; bool _roomVisualEffectsEnabled = false; bool _voiceRoomRouteVisible = false; bool _isExitingCurrentVoiceRoomSession = false; bool _isHandlingRoomRtcFailure = false; bool _roomRtcJoined = false; bool _roomRtcRoleIsBroadcaster = false; RoomStartupStatus _roomStartupStatus = RoomStartupStatus.idle; RoomStartupFailureType _roomStartupFailureType = RoomStartupFailureType.none; int _roomStartupFailureToken = 0; int _roomEntryRequestSerial = 0; final Map _relationEntryNoticeAt = {}; int? _previewRoomSeatCount; bool _currentRoomIsEntryPreview = false; bool _isHandlingRoomStartupFailure = false; Timer? _micListPollingTimer; Timer? _onlineUsersPollingTimer; Timer? _roomEntryEffectTimer; Timer? _roomRocketLaunchAnimationTimer; ValueNotifier? _roomRocketLaunchTopAvatarNotifier; final List _roomRocketGiftRefreshTimers = []; final List _roomRocketPostLaunchRefreshTimers = []; final List _roomRocketEntryReadyFallbackTimers = []; Timer? _roomRedPacketPresenceTimer; Future? _rtcEnginePrewarmTask; Future? _pendingRoomSwitchRtcLeaveTask; Future? _persistedRoomCleanupTask; 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; final Map _recentMicChangeSeatSnapshots = {}; num? _preferredSelfMicIndex; num? _pendingSelfMicGoUpIndex; int? _pendingSelfMicGoUpGuardUntilMs; num? _pendingSelfMicSourceIndex; int? _pendingSelfMicSwitchGuardUntilMs; num? _pendingSelfMicReleaseIndex; int? _pendingSelfMicReleaseGuardUntilMs; int? _seatResizeOccupantPreserveUntilMs; _RoomRtcLocalAudioRole? _lastAppliedLocalAudioRole; bool? _lastAppliedLocalAudioMuted; bool? _lastAppliedRecordingSignalMuted; int? _lastAppliedRecordingSignalVolume; bool? _lastScheduledVoiceLiveOnMic; String? _lastScheduledVoiceLiveRoomId; String? _lastScheduledAnchorRoomId; ///当前所在房间 JoinRoomRes? currenRoom; /// 声音音量变化监听 final List _onSoundVoiceChangeList = []; ///麦位 Map roomWheatMap = {}; final Map _seatEmojiEventVersions = {}; int _emojiPlaybackEventVersion = 0; final Map _roomMicRelationCache = {}; final Set _roomMicRelationLoadingUserIds = {}; final Map _roomMicRelationDismissedPairUntil = {}; final Set _roomMusicPublishingUserIds = {}; final Map _roomMusicPublishingClearTimers = {}; RoomRtcEngineAdapter? _roomRtcEngineAdapter; RoomMusicMixingStateListener? _roomMusicMixingStateListener; RoomMusicMicRouteChangedListener? _roomMusicMicRouteChangedListener; RoomMusicSessionExitListener? _roomMusicSessionExitListener; 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; bool isUserPublishingRoomMusic(String? userId) { final normalizedUserId = (userId ?? "").trim(); return normalizedUserId.isNotEmpty && _roomMusicPublishingUserIds.contains(normalizedUserId); } bool get _currentUserPublishingRoomMusic { final currentUserId = (AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim(); return isUserPublishingRoomMusic(currentUserId); } bool _isCurrentUserId(String userId) { final currentUserId = (AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim(); return currentUserId.isNotEmpty && currentUserId == userId.trim(); } Set _currentUserIdentityKeys() { return _userProfileIdentityKeys( AccountStorage().getCurrentUser()?.userProfile, ); } Set _userProfileIdentityKeys(SocialChatUserProfile? user) { return { user?.id?.trim() ?? "", user?.account?.trim() ?? "", user?.getID().trim() ?? "", }..removeWhere((item) => item.isEmpty); } bool _userProfileMatchesAnyIdentity( SocialChatUserProfile? user, Set identityKeys, ) { if (identityKeys.isEmpty) { return false; } return _userProfileIdentityKeys(user).any(identityKeys.contains); } ///房间红包列表 List redPacketList = []; num roomTaskClaimableCount = 0; initializeRealTimeCommunicationManager(BuildContext context) { this.context = context; } bool get roomVisualEffectsEnabled => _roomVisualEffectsEnabled; bool get isVoiceRoomRouteVisible => _voiceRoomRouteVisible; 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 && _voiceRoomRouteVisible; void _startVoiceRoomForegroundService() { final roomProfile = currenRoom?.roomProfile?.roomProfile; final roomName = (roomProfile?.roomName ?? "").trim(); final roomAccount = (roomProfile?.roomAccount ?? "").trim(); unawaited( SCVoiceRoomForegroundService.start( roomName: roomName.isNotEmpty ? roomName : roomAccount, ), ); } void _stopVoiceRoomForegroundService() { unawaited(SCVoiceRoomForegroundService.stop()); } bool get _shouldDeferSeatInteractionForRoomStartup => _roomStartupStatus == RoomStartupStatus.loading; void setRoomVisualEffectsEnabled(bool enabled) { if (_roomVisualEffectsEnabled == enabled) { return; } if (!enabled) { _roomEntryEffectTimer?.cancel(); _roomEntryEffectTimer = null; _roomRocketLaunchAnimationTimer?.cancel(); _roomRocketLaunchAnimationTimer = null; SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag); _releaseRoomRocketLaunchTopAvatarNotifier(); _cancelRoomRocketGiftStatusRefresh(); _cancelRoomRocketEntryReadyFallbacks(); _cancelRoomRocketPostLaunchStatusRefresh(); } _roomVisualEffectsEnabled = enabled; notifyListeners(); } void notifyRoomVisualEffectPreferenceChanged() { if (!SCGlobalConfig.isEntryVehicleAnimation) { _roomEntryEffectTimer?.cancel(); _roomEntryEffectTimer = null; } notifyListeners(); } void setVoiceRoomRouteVisible(bool visible) { if (_voiceRoomRouteVisible == visible) { return; } _voiceRoomRouteVisible = visible; if (visible) { final roomId = (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(); if (roomId.isNotEmpty) { _scheduleRoomRocketEntryReadyFallback(roomId); if (_roomStartupStatus == RoomStartupStatus.ready) { _loadRoomRocketRewardPopups(roomId); } } } 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'); } bool _isInvalidKickOffMicPrecheckError(Object error) { final normalizedMessage = _errorMessageFrom(error).toLowerCase(); return (normalizedMessage.contains('parameter') && (normalizedMessage.contains('incorrect') || normalizedMessage.contains('incomplete'))) || (normalizedMessage.contains('参数') && (normalizedMessage.contains('错误') || normalizedMessage.contains('不完整'))); } 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)); } int? _currentConfiguredRoomSeatCount() { return _normalizePreviewRoomSeatCount( currenRoom?.roomProfile?.roomSetting?.mikeSize?.toInt(), ); } void _markSeatResizeOccupantPreserveWindow({ required num? previousSeatCount, required num? nextSeatCount, }) { final previous = _normalizePreviewRoomSeatCount(previousSeatCount?.toInt()); final next = _normalizePreviewRoomSeatCount(nextSeatCount?.toInt()); if (next == null || previous == next) { return; } final hasOccupiedSeat = roomWheatMap.values.any( (seat) => (seat.user?.id ?? "").trim().isNotEmpty, ); if (!hasOccupiedSeat) { return; } _seatResizeOccupantPreserveUntilMs = DateTime.now() .add(_seatResizeOccupantPreservePeriod) .millisecondsSinceEpoch; } Map _withSeatPlaceholdersForCurrentSetting( Map source, ) { final seatCount = _currentConfiguredRoomSeatCount(); if (seatCount == null) { return source; } final roomId = currenRoom?.roomProfile?.roomProfile?.id; final nextMap = Map.from(source); for (var index = 0; index < seatCount; index += 1) { nextMap.putIfAbsent( index, () => MicRes( roomId: roomId, micIndex: index, micLock: false, micMute: false, ), ); } return nextMap; } Map _preserveMicOccupantsDuringSeatResize( Map nextMap, { required Map previousMap, }) { final preserveUntilMs = _seatResizeOccupantPreserveUntilMs; if (preserveUntilMs == null) { return nextMap; } if (preserveUntilMs <= DateTime.now().millisecondsSinceEpoch) { _seatResizeOccupantPreserveUntilMs = null; return nextMap; } final seatCount = _currentConfiguredRoomSeatCount(); if (seatCount == null) { return nextMap; } final preservedMap = Map.from(nextMap); final incomingUserIds = preservedMap.values .map((seat) => (seat.user?.id ?? "").trim()) .where((userId) => userId.isNotEmpty) .toSet(); var preservedAny = false; for (final entry in previousMap.entries) { final index = entry.key.toInt(); if (index < 0 || index >= seatCount) { continue; } final previousSeat = entry.value; final previousUser = previousSeat.user; final previousUserId = (previousUser?.id ?? "").trim(); if (previousUser == null || previousUserId.isEmpty || incomingUserIds.contains(previousUserId)) { continue; } final incomingSeat = preservedMap[entry.key]; if (incomingSeat != null && incomingSeat.user != null) { continue; } preservedMap[entry.key] = incomingSeat?.copyWith( user: previousUser, roomToken: previousSeat.roomToken, emojiPath: previousSeat.emojiPath, type: previousSeat.type, number: previousSeat.number, ) ?? previousSeat.copyWith( roomId: currenRoom?.roomProfile?.roomProfile?.id, micIndex: entry.key, ); incomingUserIds.add(previousUserId); preservedAny = true; } if (!preservedAny) { _seatResizeOccupantPreserveUntilMs = null; } return preservedMap; } 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; } final preservedMap = _preserveMicOccupantsDuringSeatResize( nextMap, previousMap: previousMap, ); final completedMap = _withSeatPlaceholdersForCurrentSetting(preservedMap); return _stabilizeSelfMicSnapshot(completedMap, previousMap: previousMap); } void _rememberMicChangeSnapshot(List mics) { if (mics.isEmpty) { return; } final nowMs = DateTime.now().millisecondsSinceEpoch; _pruneRecentMicChangeSeatSnapshots(nowMs); final expiresAtMs = nowMs + _micChangeSnapshotGracePeriod.inMilliseconds; for (final mic in mics) { final micIndex = mic.micIndex; if (micIndex == null) { continue; } _recentMicChangeSeatSnapshots[micIndex] = _RecentMicChangeSeatSnapshot( mic: _mergeKnownVipIntoMic(mic).copyWith(micIndex: micIndex), expiresAtMs: expiresAtMs, ); } } void _pruneRecentMicChangeSeatSnapshots([int? nowMs]) { if (_recentMicChangeSeatSnapshots.isEmpty) { return; } final resolvedNowMs = nowMs ?? DateTime.now().millisecondsSinceEpoch; _recentMicChangeSeatSnapshots.removeWhere( (_, snapshot) => snapshot.isExpired(resolvedNowMs), ); } Map _stabilizeMicListSnapshotWithRecentMicChange( Map nextMap, ) { final nowMs = DateTime.now().millisecondsSinceEpoch; _pruneRecentMicChangeSeatSnapshots(nowMs); if (_recentMicChangeSeatSnapshots.isEmpty) { return nextMap; } Map? stabilizedMap; final confirmedSeatIndexes = []; for (final entry in _recentMicChangeSeatSnapshots.entries) { final seatIndex = entry.key; final recentMic = entry.value.mic; final recentUserId = entry.value.userId; final currentMap = stabilizedMap ?? nextMap; final incomingMic = currentMap[seatIndex]; final incomingUserId = (incomingMic?.user?.id ?? "").trim(); if (recentUserId.isEmpty) { if (incomingUserId.isEmpty) { confirmedSeatIndexes.add(seatIndex); continue; } stabilizedMap ??= Map.from(nextMap); stabilizedMap[seatIndex] = _micChangeAuthoritativeSeat( incomingMic: incomingMic, recentMic: recentMic, seatIndex: seatIndex, clearUser: true, ); continue; } if (incomingUserId == recentUserId) { confirmedSeatIndexes.add(seatIndex); continue; } stabilizedMap ??= Map.from(nextMap); for (final otherEntry in currentMap.entries) { if (otherEntry.key == seatIndex) { continue; } if ((otherEntry.value.user?.id ?? "").trim() != recentUserId) { continue; } stabilizedMap[otherEntry.key] = otherEntry.value.copyWith( clearUser: true, ); } stabilizedMap[seatIndex] = _micChangeAuthoritativeSeat( incomingMic: incomingMic, recentMic: recentMic, seatIndex: seatIndex, ); } for (final seatIndex in confirmedSeatIndexes) { _recentMicChangeSeatSnapshots.remove(seatIndex); } return stabilizedMap ?? nextMap; } MicRes _micChangeAuthoritativeSeat({ required MicRes? incomingMic, required MicRes recentMic, required num seatIndex, bool clearUser = false, }) { final baseMic = incomingMic ?? MicRes( roomId: currenRoom?.roomProfile?.roomProfile?.id, micIndex: seatIndex, ); return baseMic.copyWith( roomId: recentMic.roomId, micIndex: recentMic.micIndex ?? seatIndex, micLock: recentMic.micLock, micMute: recentMic.micMute, user: recentMic.user, clearUser: clearUser, roomToken: recentMic.roomToken, emojiPath: recentMic.emojiPath, type: recentMic.type, number: recentMic.number, ); } void _applyMicListSnapshot( List roomWheatList, { bool notifyIfUnchanged = true, }) { final nextMap = _buildMicMap(roomWheatList); final stabilizedNextMap = _stabilizeMicListSnapshotWithRecentMicChange( nextMap, ); _applyMicSnapshot(stabilizedNextMap, notifyIfUnchanged: notifyIfUnchanged); } @visibleForTesting void debugApplyMicListSnapshot( List roomWheatList, { bool notifyIfUnchanged = true, }) { _applyMicListSnapshot(roomWheatList, notifyIfUnchanged: notifyIfUnchanged); } void _startSelfMicSwitchGuard({ required num sourceIndex, required num targetIndex, }) { _clearSelfMicGoUpGuard(); _clearSelfMicReleaseGuard(); _preferredSelfMicIndex = targetIndex; _pendingSelfMicSourceIndex = sourceIndex; _pendingSelfMicSwitchGuardUntilMs = DateTime.now().add(_selfMicStateGracePeriod).millisecondsSinceEpoch; } void _startSelfMicGoUpGuard({required num targetIndex}) { _clearSelfMicReleaseGuard(); _preferredSelfMicIndex = targetIndex; _pendingSelfMicGoUpIndex = targetIndex; _pendingSelfMicGoUpGuardUntilMs = DateTime.now().add(_selfMicStateGracePeriod).millisecondsSinceEpoch; } void _clearSelfMicGoUpGuard({bool clearPreferredIndex = false}) { _pendingSelfMicGoUpIndex = null; _pendingSelfMicGoUpGuardUntilMs = null; if (clearPreferredIndex) { _preferredSelfMicIndex = null; } } void _clearSelfMicSwitchGuard({bool clearPreferredIndex = false}) { _pendingSelfMicSourceIndex = null; _pendingSelfMicSwitchGuardUntilMs = null; if (clearPreferredIndex) { _preferredSelfMicIndex = null; } } @visibleForTesting void debugStartSelfMicGoUpGuard(num targetIndex) { _startSelfMicGoUpGuard(targetIndex: targetIndex); } 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(); _clearSelfMicGoUpGuard(); return nextMap; } final goUpIndex = _pendingSelfMicGoUpIndex; final goUpGuardUntilMs = _pendingSelfMicGoUpGuardUntilMs ?? 0; final hasActiveGoUpGuard = goUpIndex != null && goUpGuardUntilMs > nowMs; if (!hasActiveGoUpGuard) { _clearSelfMicGoUpGuard(); } else if (selfSeatIndices.isEmpty || selfSeatIndices.every((seatIndex) => seatIndex == goUpIndex)) { final stabilizedMap = _stabilizeOptimisticSelfMicSeat( nextMap: nextMap, previousMap: previousMap, selfSeatIndices: selfSeatIndices, targetIndex: goUpIndex, currentUserId: currentUserId, ); if (stabilizedMap != null) { return stabilizedMap; } } else { _clearSelfMicGoUpGuard(); } if (!hasActiveGuard) { _clearSelfMicSwitchGuard(); } else { final resolvedTargetIndex = targetIndex; final resolvedSourceIndex = sourceIndex; final selfOnlyOnSource = selfSeatIndices.isEmpty || selfSeatIndices.every( (seatIndex) => seatIndex == resolvedSourceIndex, ); if (selfOnlyOnSource) { final stabilizedMap = _stabilizeOptimisticSelfMicSeat( nextMap: nextMap, previousMap: previousMap, selfSeatIndices: selfSeatIndices, targetIndex: resolvedTargetIndex, currentUserId: currentUserId, ); if (stabilizedMap != null) { 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; } Map? _stabilizeOptimisticSelfMicSeat({ required Map nextMap, required Map previousMap, required Iterable selfSeatIndices, required num targetIndex, required String currentUserId, }) { final optimisticTargetSeat = previousMap[targetIndex]; if (optimisticTargetSeat == null || optimisticTargetSeat.user?.id != currentUserId) { return null; } final incomingTargetSeat = nextMap[targetIndex]; if (incomingTargetSeat?.user != null && incomingTargetSeat?.user?.id != currentUserId) { return null; } final stabilizedMap = Map.from(nextMap); for (final seatIndex in selfSeatIndices) { if (seatIndex == targetIndex) { continue; } final seat = stabilizedMap[seatIndex]; if (seat == null) { continue; } stabilizedMap[seatIndex] = seat.copyWith(clearUser: true); } final baseSeat = incomingTargetSeat ?? optimisticTargetSeat; stabilizedMap[targetIndex] = 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; } bool _shouldPreferDuplicateMicSeat({ required String userId, required num existingIndex, required num candidateIndex, required Map previousMap, }) { if (existingIndex == candidateIndex) { return true; } final currentUserId = (AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim(); if (userId == currentUserId && _preferredSelfMicIndex != null) { if (candidateIndex == _preferredSelfMicIndex) { return true; } if (existingIndex == _preferredSelfMicIndex) { return false; } } final candidateWasPreviouslyOccupiedByUser = previousMap[candidateIndex]?.user?.id == userId; final existingWasPreviouslyOccupiedByUser = previousMap[existingIndex]?.user?.id == userId; if (candidateWasPreviouslyOccupiedByUser != existingWasPreviouslyOccupiedByUser) { return candidateWasPreviouslyOccupiedByUser; } return false; } void _resetRoomRtcTracking() { _roomRtcJoined = false; _roomRtcRoleIsBroadcaster = false; _resetLocalAudioRuntimeTracking(); } bool get _hasCurrentRoomRtcConnection { final adapter = _roomRtcEngineAdapter; return _roomRtcJoined && adapter != null && adapter.isInRoom; } bool get _canReportMicActive { return _roomRtcJoined && _roomRtcRoleIsBroadcaster && isOnMai(); } void _resyncHeartbeatFromRoomRtcState() { final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? ""; final canReportMicActive = _canReportMicActive; _syncRoomHeartbeatState( roomId: roomId, isOnMic: canReportMicActive, shouldRunAnchorHeartbeat: canReportMicActive, ); } MicRes? _currentUserMicSeat() { final currentUserId = (AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim(); if (currentUserId.isEmpty) { return null; } for (final mic in roomWheatMap.values) { if ((mic.user?.id ?? "").trim() == currentUserId) { return mic; } } return null; } void _syncSelfMicRuntimeState() { final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id; if ((currentUserId ?? "").isEmpty) { return; } final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? ""; final currentUserMic = _currentUserMicSeat(); if (currentUserMic == null) { _clearSelfMicSwitchGuard(clearPreferredIndex: true); _clearSelfMicGoUpGuard(clearPreferredIndex: true); _roomRtcRoleIsBroadcaster = false; _syncRoomHeartbeatState( roomId: roomId, isOnMic: false, shouldRunAnchorHeartbeat: false, ); _applyLocalAudioRuntimeState( role: _RoomRtcLocalAudioRole.audience, muted: true, ); _roomMusicMicRouteChangedListener?.call(false); return; } _preferredSelfMicIndex = currentUserMic.micIndex; final canReportMicActive = _canReportMicActive; _syncRoomHeartbeatState( roomId: roomId, isOnMic: canReportMicActive, shouldRunAnchorHeartbeat: canReportMicActive, ); _applyLocalAudioRuntimeState( role: _RoomRtcLocalAudioRole.anchor, 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 (!_roomRtcJoined) { 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 _RoomRtcLocalAudioRole role, required bool muted, }) { if (role != _RoomRtcLocalAudioRole.anchor) { _roomRtcRoleIsBroadcaster = false; } final adapter = _roomRtcEngineAdapter; if (!_roomRtcJoined || adapter == null) { _lastAppliedLocalAudioRole = null; _lastAppliedLocalAudioMuted = null; _lastAppliedRecordingSignalMuted = null; _lastAppliedRecordingSignalVolume = null; return; } final shouldPublishLocalAudio = role == _RoomRtcLocalAudioRole.anchor; final keepAudioPublishing = shouldPublishLocalAudio && muted && _currentUserPublishingRoomMusic; final localAudioMuted = !shouldPublishLocalAudio || (muted && !keepAudioPublishing); final recordingSignalMuted = !shouldPublishLocalAudio || muted; final recordingSignalVolume = recordingSignalMuted ? 0 : 100; if (_lastAppliedLocalAudioRole == role && _lastAppliedLocalAudioMuted == localAudioMuted && _lastAppliedRecordingSignalMuted == recordingSignalMuted && _lastAppliedRecordingSignalVolume == recordingSignalVolume) { return; } if (shouldPublishLocalAudio) { unawaited( _switchRoomRtcToBroadcaster( muted: muted, keepAudioPublishing: keepAudioPublishing, ), ); } else { unawaited(_switchRoomRtcToAudience()); } } void _resetHeartbeatTracking() { _lastScheduledVoiceLiveOnMic = null; _lastScheduledVoiceLiveRoomId = null; _lastScheduledAnchorRoomId = null; } void _resetLocalAudioRuntimeTracking() { _lastAppliedLocalAudioRole = null; _lastAppliedLocalAudioMuted = null; _lastAppliedRecordingSignalMuted = null; _lastAppliedRecordingSignalVolume = null; } void _applyMicSnapshot( Map nextMap, { bool notifyIfUnchanged = true, }) { final sanitizedNextMap = _withoutLocallyDismissedRoomMicRelationsFromMicMap( nextMap, ); final changed = !identical(sanitizedNextMap, nextMap) || !_sameMicMaps(roomWheatMap, sanitizedNextMap); roomWheatMap = sanitizedNextMap; final relationCacheChanged = _mergeCachedRoomMicRelationProfiles(); _scheduleRoomMicRelationHydration(); _syncSelfMicRuntimeState(); final roomMusicChanged = _pruneRoomMusicPublishingUsersToMicSeats(); if (changed || notifyIfUnchanged || roomMusicChanged || relationCacheChanged) { notifyListeners(); } } void invalidateRoomMicRelationBetween({ required String? userId, required String? peerUserId, String? relationType, }) { final currentUserId = (AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim(); final leftUserId = (userId ?? "").trim().isNotEmpty ? userId!.trim() : currentUserId; final rightUserId = (peerUserId ?? "").trim(); if (leftUserId.isEmpty || rightUserId.isEmpty || leftUserId == rightUserId) { return; } final now = DateTime.now(); _pruneRoomMicRelationDismissedPairs(now); final normalizedType = scNormalizeCpRelationType(relationType); final dismissedUntil = now.add(_roomMicRelationDismissedPairTtl); final leftKeys = _roomMicRelationIdentityKeysForUserId(leftUserId); final rightKeys = _roomMicRelationIdentityKeysForUserId(rightUserId); for (final leftKey in leftKeys) { for (final rightKey in rightKeys) { _roomMicRelationDismissedPairUntil[_roomMicRelationDismissedPairKey( leftKey, rightKey, normalizedType, )] = dismissedUntil; } } final dismissedUserIds = {leftUserId, rightUserId}; _roomMicRelationCache.removeWhere( (cacheUserId, _) => dismissedUserIds.contains(cacheUserId), ); _roomMicRelationLoadingUserIds.removeWhere(dismissedUserIds.contains); final nextMap = _withoutLocallyDismissedRoomMicRelationsFromMicMap( roomWheatMap, ); if (!identical(nextMap, roomWheatMap)) { roomWheatMap = nextMap; notifyListeners(); } requestMicrophoneListRefresh( notifyIfUnchanged: false, minInterval: const Duration(milliseconds: 350), ); } void refreshRoomMicRelationProfilesForUsers(Set userIds) { final roomId = (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(); if (roomId.isEmpty || roomWheatMap.isEmpty) { return; } final normalizedUserIds = userIds.map((userId) => userId.trim()).where((userId) { return userId.isNotEmpty; }).toSet(); final refreshAll = normalizedUserIds.isEmpty; if (refreshAll) { _roomMicRelationCache.clear(); _roomMicRelationLoadingUserIds.clear(); } else { _roomMicRelationCache.removeWhere((userId, _) { return normalizedUserIds.contains(userId); }); _roomMicRelationLoadingUserIds.removeWhere(normalizedUserIds.contains); } final queuedUserIds = {}; for (final mic in roomWheatMap.values) { final user = mic.user; final userId = _roomMicRelationUserId(user); if (user == null || userId.isEmpty) { continue; } if (!refreshAll && !normalizedUserIds.contains(userId)) { continue; } if (!queuedUserIds.add(userId) || !_roomMicRelationLoadingUserIds.add(userId)) { continue; } unawaited(_hydrateRoomMicRelationProfile(roomId, userId, user)); } } bool _mergeCachedRoomMicRelationProfiles() { final now = DateTime.now(); _roomMicRelationCache.removeWhere( (_, cache) => cache.isExpired(now, _roomMicRelationCacheTtl), ); var changed = false; final mergedMap = roomWheatMap.map((index, mic) { final user = mic.user; final userId = _roomMicRelationUserId(user); if (user == null || userId.isEmpty) { return MapEntry(index, mic); } final cache = _roomMicRelationCache[userId]; if (cache == null || cache.isExpired(now, _roomMicRelationCacheTtl)) { return MapEntry(index, mic); } final mergedUser = _mergeRoomMicRelationProfile(user, cache.profile); if (identical(mergedUser, user)) { return MapEntry(index, mic); } changed = true; return MapEntry(index, mic.copyWith(user: mergedUser)); }); if (changed) { roomWheatMap = mergedMap; } return changed; } void _scheduleRoomMicRelationHydration() { final roomId = (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(); if (roomId.isEmpty || roomWheatMap.isEmpty) { return; } final now = DateTime.now(); final queuedUserIds = {}; for (final mic in roomWheatMap.values) { final user = mic.user; final userId = _roomMicRelationUserId(user); if (user == null || userId.isEmpty || !queuedUserIds.add(userId)) { continue; } final cache = _roomMicRelationCache[userId]; if (cache == null && _hasCompleteRoomMicRelationLists(user)) { continue; } if (cache != null && !cache.isExpired(now, _roomMicRelationCacheTtl)) { continue; } if (!_roomMicRelationLoadingUserIds.add(userId)) { continue; } unawaited(_hydrateRoomMicRelationProfile(roomId, userId, user)); } } Future _hydrateRoomMicRelationProfile( String roomId, String userId, SocialChatUserProfile profile, ) async { try { final hydratedProfile = await _loadRoomMicRelationProfile( profile, userId, ); _roomMicRelationCache[userId] = _RoomMicRelationProfileCache( profile: hydratedProfile, loadedAt: DateTime.now(), ); _applyHydratedRoomMicRelationProfile(roomId, userId, hydratedProfile); } finally { _roomMicRelationLoadingUserIds.remove(userId); } } Future _loadRoomMicRelationProfile( SocialChatUserProfile profile, String userId, ) async { final cpPairs = []; final closeFriendPairs = []; await Future.wait( _roomMicRelationTypes.map((relationType) async { final pairs = await _safeLoadRoomMicRelationPairs(userId, relationType); for (final pair in pairs) { if (!_hasRoomMicRelationPair(pair)) { continue; } final normalizedType = scNormalizeCpRelationType( pair.relationType ?? relationType, ); final enrichedPair = pair.copyWith(relationType: normalizedType); if (_isRoomMicRelationLocallyDismissed(enrichedPair)) { continue; } if (normalizedType == "CP") { cpPairs.add(enrichedPair); } else { closeFriendPairs.add(enrichedPair); } } }), ); return profile.copyWith( cpList: cpPairs, closeFriendList: closeFriendPairs, isCpRelation: cpPairs.isNotEmpty, ); } Future> _safeLoadRoomMicRelationPairs( String userId, String relationType, ) async { try { return await SCAccountRepository().cpRelationshipPairs( userId: userId, relationType: relationType, ); } catch (error) { if (kDebugMode) { debugPrint( "[CP][MicRelation] pair failed userId=$userId " "relationType=$relationType error=$error", ); } return const []; } } void _applyHydratedRoomMicRelationProfile( String roomId, String userId, SocialChatUserProfile hydratedProfile, ) { final currentRoomId = (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(); if (currentRoomId != roomId) { return; } var changed = false; final mergedMap = roomWheatMap.map((index, mic) { final user = mic.user; if (user == null || _roomMicRelationUserId(user) != userId) { return MapEntry(index, mic); } final mergedUser = _mergeRoomMicRelationProfile(user, hydratedProfile); if (identical(mergedUser, user)) { return MapEntry(index, mic); } changed = true; return MapEntry(index, mic.copyWith(user: mergedUser)); }); if (!changed) { return; } roomWheatMap = mergedMap; notifyListeners(); } SocialChatUserProfile _mergeRoomMicRelationProfile( SocialChatUserProfile user, SocialChatUserProfile relationProfile, ) { final nextCpList = relationProfile.cpList ?? const []; final nextCloseFriendList = relationProfile.closeFriendList ?? const []; if (_sameRoomMicRelationLists(user.cpList, nextCpList) && _sameRoomMicRelationLists(user.closeFriendList, nextCloseFriendList)) { return user; } return user.copyWith( cpList: nextCpList, closeFriendList: nextCloseFriendList, isCpRelation: nextCpList.isNotEmpty, ); } bool _hasCompleteRoomMicRelationLists(SocialChatUserProfile profile) { return profile.cpList != null && profile.closeFriendList != null; } bool _hasRoomMicRelationPair(CPRes? pair) { return (pair?.cpUserId?.trim().isNotEmpty ?? false) || (pair?.cpAccount?.trim().isNotEmpty ?? false) || (pair?.meUserId?.trim().isNotEmpty ?? false) || (pair?.meAccount?.trim().isNotEmpty ?? false); } bool _sameRoomMicRelationLists(List? left, List? right) { final leftList = left ?? const []; final rightList = right ?? const []; if (leftList.length != rightList.length) { return false; } for (var index = 0; index < leftList.length; index += 1) { if (_roomMicRelationPairKey(leftList[index]) != _roomMicRelationPairKey(rightList[index])) { return false; } } return true; } String _roomMicRelationPairKey(CPRes pair) { return [ pair.meUserId?.trim() ?? "", pair.meAccount?.trim() ?? "", pair.cpUserId?.trim() ?? "", pair.cpAccount?.trim() ?? "", pair.relationType?.trim() ?? "", pair.status?.trim() ?? "", pair.days?.trim() ?? "", pair.levelText?.trim() ?? "", pair.cpValue?.toString() ?? "", pair.dismissCost?.toString() ?? "", ].join("|"); } String _roomMicRelationUserId(SocialChatUserProfile? user) { return user?.id?.trim() ?? ""; } Map _withoutLocallyDismissedRoomMicRelationsFromMicMap( Map sourceMap, ) { if (_roomMicRelationDismissedPairUntil.isEmpty || sourceMap.isEmpty) { return sourceMap; } _pruneRoomMicRelationDismissedPairs(DateTime.now()); if (_roomMicRelationDismissedPairUntil.isEmpty) { return sourceMap; } var changed = false; final nextMap = sourceMap.map((index, mic) { final user = mic.user; if (user == null) { return MapEntry(index, mic); } final nextUser = _withoutLocallyDismissedRoomMicRelations(user); if (identical(nextUser, user)) { return MapEntry(index, mic); } changed = true; return MapEntry(index, mic.copyWith(user: nextUser)); }); return changed ? nextMap : sourceMap; } SocialChatUserProfile _withoutLocallyDismissedRoomMicRelations( SocialChatUserProfile user, ) { final nextCpList = _withoutLocallyDismissedRoomMicRelationList(user.cpList); final nextCloseFriendList = _withoutLocallyDismissedRoomMicRelationList( user.closeFriendList, ); if (identical(nextCpList, user.cpList) && identical(nextCloseFriendList, user.closeFriendList)) { return user; } final effectiveCpList = nextCpList ?? user.cpList ?? const []; return user.copyWith( cpList: nextCpList, closeFriendList: nextCloseFriendList, isCpRelation: effectiveCpList.isNotEmpty, ); } List? _withoutLocallyDismissedRoomMicRelationList( List? sourceList, ) { if (sourceList == null || sourceList.isEmpty) { return sourceList; } final filtered = sourceList .where((relation) => !_isRoomMicRelationLocallyDismissed(relation)) .toList(growable: false); return filtered.length == sourceList.length ? sourceList : filtered; } bool _isRoomMicRelationLocallyDismissed(CPRes relation) { if (_roomMicRelationDismissedPairUntil.isEmpty) { return false; } _pruneRoomMicRelationDismissedPairs(DateTime.now()); if (_roomMicRelationDismissedPairUntil.isEmpty) { return false; } final keys = _roomMicRelationDismissedPairKeysFromRelation(relation); return keys.any(_roomMicRelationDismissedPairUntil.containsKey); } Set _roomMicRelationDismissedPairKeysFromRelation(CPRes relation) { final leftKeys = { relation.meUserId?.trim() ?? "", relation.meAccount?.trim() ?? "", }..remove(""); final rightKeys = { relation.cpUserId?.trim() ?? "", relation.cpAccount?.trim() ?? "", }..remove(""); if (leftKeys.isEmpty || rightKeys.isEmpty) { return const {}; } final relationType = scNormalizeCpRelationType(relation.relationType); return { for (final leftKey in leftKeys) for (final rightKey in rightKeys) _roomMicRelationDismissedPairKey(leftKey, rightKey, relationType), }; } String _roomMicRelationDismissedPairKey( String leftIdentity, String rightIdentity, String relationType, ) { final identities = [leftIdentity.trim(), rightIdentity.trim()] ..sort(); return '${scNormalizeCpRelationType(relationType)}|' '${identities[0]}|${identities[1]}'; } Set _roomMicRelationIdentityKeysForUserId(String userId) { final normalizedUserId = userId.trim(); final keys = {normalizedUserId}; for (final mic in roomWheatMap.values) { final user = mic.user; if ((user?.id ?? "").trim() == normalizedUserId) { keys ..add(user?.id?.trim() ?? "") ..add(user?.account?.trim() ?? ""); } } return keys..remove(""); } void _pruneRoomMicRelationDismissedPairs(DateTime now) { _roomMicRelationDismissedPairUntil.removeWhere( (_, expiresAt) => !expiresAt.isAfter(now), ); } bool _pruneRoomMusicPublishingUsersToMicSeats() { if (_roomMusicPublishingUserIds.isEmpty) { return false; } final seatedUserIds = roomWheatMap.values .map((seat) => (seat.user?.id ?? "").trim()) .where((userId) => userId.isNotEmpty) .toSet(); final removedUserIds = _roomMusicPublishingUserIds .where((userId) => !seatedUserIds.contains(userId)) .toList(); for (final userId in removedUserIds) { _roomMusicPublishingUserIds.remove(userId); _roomMusicPublishingClearTimers.remove(userId)?.cancel(); } return removedUserIds.isNotEmpty; } 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 _cleanupRoomRtcState({ bool clearMicSeats = false, String? onlyIfBusinessRoomId, }) async { final expectedRoomId = (onlyIfBusinessRoomId ?? "").trim(); if (expectedRoomId.isNotEmpty) { final currentRoomId = (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(); if (currentRoomId.isNotEmpty && currentRoomId != expectedRoomId) { return; } } _resetRoomRtcTracking(); try { await _roomRtcEngineAdapter?.leaveRoom(); } catch (error) {} if (clearMicSeats) { _roomMicRelationCache.clear(); _roomMicRelationLoadingUserIds.clear(); _roomMicRelationDismissedPairUntil.clear(); roomWheatMap.clear(); notifyListeners(); } _resyncHeartbeatFromRoomRtcState(); } Future _leaveRoomRtcForExit() async { final adapter = _roomRtcEngineAdapter; _resetRoomRtcTracking(); if (adapter == null) { return; } try { await adapter.leaveRoom(); } catch (error) {} } Future _handleRoomRtcJoinFailed( Object error, { required bool exitRoom, }) async { await _cleanupRoomRtcState(clearMicSeats: true); if (!exitRoom || _isExitingCurrentVoiceRoomSession) { return; } SCTts.show("Join room fail"); unawaited(exitCurrentVoiceRoomSession(false)); } Future _handleRoomRtcDisconnected(Object reason) async { if (_isHandlingRoomRtcFailure || _isExitingCurrentVoiceRoomSession) { return; } _isHandlingRoomRtcFailure = true; SCTts.show("Voice connection failed"); await _cleanupRoomRtcState(clearMicSeats: true); await exitCurrentVoiceRoomSession(false); } Future prewarmRtcEngine() { final existingTask = _rtcEnginePrewarmTask; if (existingTask != null) { return existingTask; } final task = _currentRoomRtcEngineAdapter.prewarm(); _rtcEnginePrewarmTask = task; unawaited( task.whenComplete(() { if (identical(_rtcEnginePrewarmTask, task)) { _rtcEnginePrewarmTask = null; } }), ); return task; } RoomRtcEngineAdapter get _currentRoomRtcEngineAdapter { return _roomRtcEngineAdapter ??= TrtcRoomRtcEngineAdapter( onVoiceVolume: _handleRoomRtcVoiceVolume, onAudioMixingStateChanged: _handleRoomRtcAudioMixingStateChanged, onRoomError: (error) => unawaited(_handleRoomRtcDisconnected(error)), onRoomExit: (reason) => unawaited(_handleRoomRtcDisconnected('TRTC exit: $reason')), ); } String _resolveTrtcUserSigForCurrentUser() { final userSig = (AccountStorage().getCurrentUser()?.userSig ?? "").trim(); if (userSig.isEmpty) { throw StateError('TRTC userSig is empty'); } return userSig; } Future _fetchRoomRtcJoinCredentialForCurrentRoom() async { return _resolveTrtcUserSigForCurrentUser(); } Future<_RoomStartupAsyncResult> _captureStartupFuture( Future future, ) async { try { return _RoomStartupAsyncResult.value(await future); } catch (error, stackTrace) { return _RoomStartupAsyncResult.error(error, stackTrace); } } Future joinRoomRtcVoiceChannel({ bool throwOnError = false, required String trtcUserSig, }) async { final identity = RoomRtcSessionIdentity.fromJoinRoom( roomProfile: currenRoom?.roomProfile?.roomProfile, userProfile: AccountStorage().getCurrentUser()?.userProfile, ); if (!identity.isValid || identity.businessRoomId.isEmpty) { final error = StateError('TRTC room identity is invalid'); await _handleRoomRtcJoinFailed(error, exitRoom: !throwOnError); if (throwOnError) { throw error; } return; } final pendingRoomSwitchLeaveTask = _pendingRoomSwitchRtcLeaveTask; if (pendingRoomSwitchLeaveTask != null) { try { await pendingRoomSwitchLeaveTask; } catch (error) {} } _roomRtcJoined = false; _roomRtcRoleIsBroadcaster = false; try { final adapter = _currentRoomRtcEngineAdapter; await adapter.enterRoomAsAudience( RoomRtcEnterRoomConfig( identity: identity, trtcSdkAppId: SCGlobalConfig.trtcSdkAppId, trtcUserSig: trtcUserSig, throwOnError: true, ), ); _roomRtcJoined = true; _roomRtcRoleIsBroadcaster = false; _syncSelfMicRuntimeState(); adapter.muteAllRemoteAudio(roomIsMute); } catch (e) { await _handleRoomRtcJoinFailed(e, exitRoom: !throwOnError); if (throwOnError) { rethrow; } } } Future _switchRoomRtcToBroadcaster({ required bool muted, bool keepAudioPublishing = false, }) async { if (!_roomRtcJoined || _roomRtcEngineAdapter == null) { return false; } try { final localAudioMuted = muted && !keepAudioPublishing; final switched = await _currentRoomRtcEngineAdapter.switchToAnchor( RoomRtcAnchorConfig(muted: muted, publishMuted: localAudioMuted), ); if (!switched) { _roomRtcRoleIsBroadcaster = false; _resyncHeartbeatFromRoomRtcState(); return false; } _lastAppliedLocalAudioRole = _RoomRtcLocalAudioRole.anchor; _lastAppliedLocalAudioMuted = localAudioMuted; _lastAppliedRecordingSignalMuted = muted; _lastAppliedRecordingSignalVolume = muted ? 0 : 100; _roomRtcRoleIsBroadcaster = true; _resyncHeartbeatFromRoomRtcState(); return true; } catch (error) { _roomRtcRoleIsBroadcaster = false; _resyncHeartbeatFromRoomRtcState(); return false; } } Future _rejoinCurrentRoomRtcForSameRoomReuse(String roomId) async { if (_hasCurrentRoomRtcConnection) { _setRoomStartupReady(); if (_voiceRoomRouteVisible) { _loadRoomRocketRewardPopups(roomId); } return; } try { final trtcUserSig = await _fetchRoomRtcJoinCredentialForCurrentRoom(); if (!_isCurrentRoomId(roomId)) { return; } await joinRoomRtcVoiceChannel( throwOnError: true, trtcUserSig: trtcUserSig, ); if (!_isCurrentRoomId(roomId)) { await _cleanupRoomRtcState( clearMicSeats: currenRoom == null, onlyIfBusinessRoomId: roomId, ); return; } await _bootstrapRoomHeartbeatState(); if (!_isCurrentRoomId(roomId)) { return; } _setRoomStartupReady(); if (_voiceRoomRouteVisible) { _loadRoomRocketRewardPopups(roomId); } } catch (error) { if (!_isCurrentRoomId(roomId)) { return; } _stopVoiceRoomForegroundService(); _setRoomStartupFailed(RoomStartupFailureType.rtc); } } Future _switchRoomRtcToAudience() async { try { final switched = await _currentRoomRtcEngineAdapter.switchToAudience(); if (!switched) { return false; } _lastAppliedLocalAudioRole = _RoomRtcLocalAudioRole.audience; _lastAppliedLocalAudioMuted = true; _lastAppliedRecordingSignalMuted = true; _lastAppliedRecordingSignalVolume = 0; _roomRtcRoleIsBroadcaster = false; _resyncHeartbeatFromRoomRtcState(); return true; } catch (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 (_roomRtcJoined && isOnMai()) { await _switchRoomRtcToBroadcaster( muted: true, keepAudioPublishing: _currentUserPublishingRoomMusic, ); } } else { await _switchRoomRtcToBroadcaster(muted: false); } notifyListeners(); } Future handleSelfMicRemovedByRemote({ bool refreshMicList = true, }) async { final currentUserId = (AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim(); if (currentUserId.isEmpty) { await _switchRoomRtcToAudience(); return; } final currentSeatIndex = userOnMaiInIndex(currentUserId); isMic = true; _clearSelfMicSwitchGuard(clearPreferredIndex: true); _clearSelfMicGoUpGuard(clearPreferredIndex: true); if (currentSeatIndex > -1) { _startSelfMicReleaseGuard(sourceIndex: currentSeatIndex); } else { _clearSelfMicReleaseGuard(); } _clearUserFromSeats(currentUserId); _roomRtcRoleIsBroadcaster = false; _syncSelfMicRuntimeState(); notifyListeners(); await _switchRoomRtcToAudience(); if (refreshMicList) { requestMicrophoneListRefresh( notifyIfUnchanged: false, minInterval: const Duration(milliseconds: 350), ); } } Future releaseRtcEngineForAppTermination() async { final adapter = _roomRtcEngineAdapter; _roomRtcEngineAdapter = null; _rtcEnginePrewarmTask = null; _resetRoomRtcTracking(); try { await adapter?.release(); } catch (e) {} await SCVoiceRoomForegroundService.stop(); } void _handleRoomRtcVoiceVolume( List userVolumes, int totalVolume, ) { if (roomWheatMap.isEmpty || userVolumes.isEmpty) { return; } for (final seat in roomWheatMap.values) { seat.setVolume = 0; } final seatIndexByUserId = {}; roomWheatMap.forEach((seatIndex, mic) { final userId = (mic.user?.id ?? "").trim(); if (userId.isEmpty) { return; } seatIndexByUserId[userId] = seatIndex; }); final listeners = List.from(_onSoundVoiceChangeList); for (final info in userVolumes) { final volume = info.volume; if (volume <= 0) { continue; } final seatIndex = seatIndexByUserId[info.userId.trim()]; if (seatIndex == null) { continue; } if (!shouldShowSeatSoundWave(seatIndex)) { continue; } roomWheatMap[seatIndex]?.setVolume = volume; for (final listener in listeners) { listener(seatIndex, volume); } } } void _handleRoomRtcAudioMixingStateChanged( RoomRtcAudioMixingState state, RoomRtcAudioMixingReason reason, ) { switch (state) { case RoomRtcAudioMixingState.playing: isMusicPlaying = true; break; case RoomRtcAudioMixingState.stopped: case RoomRtcAudioMixingState.failed: isMusicPlaying = false; break; case RoomRtcAudioMixingState.paused: break; } _roomMusicMixingStateListener?.call(state, reason); } ///关注/取消房间 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 { SCFloatIchart().remove(); 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 (!context.mounted) { return; } final persistedRoomCleanupRequest = _preparePersistedVoiceRoomCleanupBeforeEntry( targetRoomId: roomId, restoreOnlineHeartbeatWhenIdle: false, ); if (roomId == currenRoom?.roomProfile?.roomProfile?.id) { if (_currentRoomIsEntryPreview || _roomStartupStatus == RoomStartupStatus.loading) { if (!context.mounted) { return; } _startVoiceRoomForegroundService(); VoiceRoomRoute.openVoiceRoom(context); _startPersistedVoiceRoomCleanupInBackground( persistedRoomCleanupRequest, ); return; } ///最小化进入房间,或者进入的是同一个房间 final loaded = await loadRoomInfo( currenRoom?.roomProfile?.roomProfile?.id ?? "", ); if (!loaded) { return; } if (!context.mounted) { return; } Provider.of( context, listen: false, ).fetchUserProfileData(); final groupId = currenRoom?.roomProfile?.roomProfile?.roomAccount ?? ""; if (groupId.isNotEmpty) { unawaited( rtmProvider?.loadRoomHistoryMessages(groupId, count: 20) ?? Future.value(), ); } retrieveMicrophoneList(); fetchOnlineUsersList(); _startRoomStatePolling(); final shouldRejoinRoomRtc = !_hasCurrentRoomRtcConnection; if (shouldRejoinRoomRtc) { _setRoomStartupLoading(); } else { _setRoomStartupReady(); } setRoomVisualEffectsEnabled(true); _startRoomRedPacketPresenceHeartbeat(roomId); _refreshRoomRedPacketListAfterEntry(roomId: roomId); _startVoiceRoomForegroundService(); VoiceRoomRoute.openVoiceRoom(context); _startPersistedVoiceRoomCleanupInBackground(persistedRoomCleanupRequest); if (shouldRejoinRoomRtc) { unawaited(_rejoinCurrentRoomRtcForSameRoomReuse(roomId)); } } else { JoinRoomRes? preEnteredRoom; if (pwd == null && _previewRoomRequiresPassword(context, previewData)) { preEnteredRoom = await _showLockedRoomEntryDialog( roomId, needOpenRedenvelope: needOpenRedenvelope, redPackId: redPackId, ); if (preEnteredRoom == null || !context.mounted) { return; } } if (currenRoom != null) { await _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, ); _startVoiceRoomForegroundService(); VoiceRoomRoute.openVoiceRoom(context); _startPersistedVoiceRoomCleanupInBackground(persistedRoomCleanupRequest); notifyListeners(); final entryRequestSerial = ++_roomEntryRequestSerial; if (preEnteredRoom != null) { unawaited( _completeEnteredVoiceRoomSession( preEnteredRoom, entryRequestSerial: entryRequestSerial, needOpenRedenvelope: needOpenRedenvelope, redPackId: redPackId, ), ); } else { 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, silentErrorToast: true, ); await _completeEnteredVoiceRoomSession( enteredRoom, entryRequestSerial: entryRequestSerial, needOpenRedenvelope: needOpenRedenvelope, redPackId: redPackId, ); } catch (e, _) { if (!_isActiveRoomEntryRequest(entryRequestSerial)) { return; } if (pwd == null && _isRoomPasswordError(e)) { final enteredRoom = await _showLockedRoomEntryDialog( roomId, needOpenRedenvelope: needOpenRedenvelope, redPackId: redPackId, ); if (enteredRoom != null && _isActiveRoomEntryRequest(entryRequestSerial)) { await _completeEnteredVoiceRoomSession( enteredRoom, entryRequestSerial: entryRequestSerial, needOpenRedenvelope: needOpenRedenvelope, redPackId: redPackId, ); return; } } _stopVoiceRoomForegroundService(); _setRoomStartupFailed( _isKickedOutRoomEntryError(e) ? RoomStartupFailureType.kicked : RoomStartupFailureType.entry, ); } } Future _completeEnteredVoiceRoomSession( JoinRoomRes enteredRoom, { required int entryRequestSerial, bool needOpenRedenvelope = false, String redPackId = "", }) async { if (!_isActiveRoomEntryRequest(entryRequestSerial)) { unawaited(_cleanupStaleEnteredRoom(enteredRoom)); return; } currenRoom = _mergeVoiceRoomVipHints(enteredRoom, currenRoom); _syncCurrentUserVipHintFromRoom(currenRoom); _startVoiceRoomForegroundService(); unawaited(_refreshCurrentUserVipHintForRoom(entryRequestSerial)); _currentRoomIsEntryPreview = false; _previewRoomSeatCount = null; notifyListeners(); await initializeRoomSession( entryRequestSerial: entryRequestSerial, needOpenRedenvelope: needOpenRedenvelope, redPackId: redPackId, shouldOpenRoomPage: false, ); } bool _previewRoomRequiresPassword( BuildContext context, RoomEntryPreviewData? previewData, ) { if (previewData?.existsPassword != true) { return false; } final ownerId = (previewData?.userId ?? "").trim(); final currentUserId = (AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim(); if (ownerId.isNotEmpty && ownerId == currentUserId) { return false; } final userIdentity = Provider.of( context, listen: false, ).userIdentity; if (userIdentity?.superAdmin == true || userIdentity?.admin == true || userIdentity?.yumiManager == true) { return false; } return true; } Future _showLockedRoomEntryDialog( String roomId, { bool needOpenRedenvelope = false, String redPackId = "", }) async { JoinRoomRes? enteredRoom; await RoomPasswordDialog.show( onSubmit: (password) async { try { enteredRoom = await SCAccountRepository().entryRoom( roomId, pwd: password, redPackId: redPackId, needOpenRedenvelope: needOpenRedenvelope, silentErrorToast: true, ); return null; } catch (error) { if (_isRoomPasswordError(error)) { return "Password is incorrect"; } if (_isKickedOutRoomEntryError(error)) { return SCAppLocalizations.of(context!)!.roomKickedEntryBlockedTips; } final message = _readNetworkErrorMessage(error); return message.isNotEmpty ? message : "Failed to enter room"; } }, ); return enteredRoom; } bool _isRoomPasswordError(Object error) { return _readNetworkErrorCode(error) == _roomPasswordNotTrueErrorCode || _readNetworkErrorCodeName(error) == "PASSWORD_NOT_TRUE"; } bool _isKickedOutRoomEntryError(Object error) { final codeName = _readNetworkErrorCodeName(error).toUpperCase(); if (codeName.contains("BLACK") || codeName.contains("KICK") || codeName.contains("BAN")) { return true; } final message = _readNetworkErrorMessage(error).toLowerCase(); if (message.contains("blacklist") || message.contains("kicked") || message.contains("kick") || message.contains("ban") || message.contains("请出") || message.contains("踢") || message.contains("拉黑")) { return true; } return message == "操作失败" || message == "operation failed"; } int? _readNetworkErrorCode(Object error) { if (error is DioException) { final responseData = error.response?.data; if (responseData is Map) { final value = responseData["errorCode"] ?? responseData["code"]; if (value is int) { return value; } if (value is num) { return value.toInt(); } if (value is String) { return int.tryParse(value); } } } return null; } String _readNetworkErrorCodeName(Object error) { if (error is DioException) { final responseData = error.response?.data; if (responseData is Map) { return (responseData["errorCodeName"] ?? responseData["code"] ?? "") .toString() .trim(); } } return ""; } String _readNetworkErrorMessage(Object error) { if (error is DioException) { final responseData = error.response?.data; if (responseData is Map) { return (responseData["errorMsg"] ?? responseData["message"] ?? "") .toString() .trim(); } return (error.error ?? error.message ?? "").toString().trim(); } return error.toString().trim(); } 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 needsVipStatus = existingVipLevel == null || currentProfile.getHeaddress() == null; final vipStatus = needsVipStatus ? await _loadCurrentVipStatus() : null; final vipLevel = existingVipLevel ?? _vipLevelFromStatus(vipStatus); if (vipLevel == null || vipLevel.isEmpty) { return; } final updatedProfile = socialChatProfileWithAvatarFrameResource( currentProfile.copyWith(vipLevel: vipLevel), _avatarFramePropsFromVipStatus(vipStatus), expireTime: _expireTimeFromVipStatus(vipStatus), ); if (!_profileHasPositiveVipLevelHint(currentProfile) || currentProfile.getHeaddress()?.sourceUrl != updatedProfile.getHeaddress()?.sourceUrl) { 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 _loadCurrentVipStatus() async { final repository = SCVipRepositoryImp(); try { final home = await repository.vipHome(); final state = home.state; if (state != null && state.levelInt > 0) { return state; } } catch (_) {} try { return 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'; } PropsResources? _avatarFramePropsFromVipStatus(SCVipStatusRes? status) { if (status?.isActive != true) { return null; } final resource = status?.avatarFrame; return socialChatAvatarFrameProps( id: resource?.resourceId, name: resource?.name, sourceUrl: resource?.sourceResourceUrl, cover: resource?.coverUrl ?? resource?.cover, code: "VIP_AVATAR_FRAME", ); } String? _expireTimeFromVipStatus(SCVipStatusRes? status) { final expireAt = status?.expireAt?.trim(); if (expireAt == null || expireAt.isEmpty) { return null; } final normalized = expireAt.contains("T") ? expireAt : expireAt.replaceFirst(" ", "T"); return DateTime.tryParse(normalized)?.millisecondsSinceEpoch.toString(); } 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)) { return; } await _retryExitNetworkRequest('quit stale entered room $staleRoomId', () { return SCAccountRepository() .quitRoom(staleRoomId, silentErrorToast: true) .then((didQuit) { if (!didQuit) { throw StateError('quitRoom returned false'); } }); }); } Future cleanupPersistedVoiceRoomSessionBeforeEntry({ String? targetRoomId, bool restoreOnlineHeartbeatWhenIdle = true, }) { final cleanupRequest = _preparePersistedVoiceRoomCleanupBeforeEntry( targetRoomId: targetRoomId, restoreOnlineHeartbeatWhenIdle: restoreOnlineHeartbeatWhenIdle, ); return _runPersistedVoiceRoomCleanup(cleanupRequest); } _PersistedVoiceRoomCleanupRequest? _preparePersistedVoiceRoomCleanupBeforeEntry({ String? targetRoomId, bool restoreOnlineHeartbeatWhenIdle = true, }) { final staleRoomId = DataPersistence.getLastTimeRoomId().trim(); if (staleRoomId.isEmpty) { return null; } final currentRoomId = (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(); if (currentRoomId.isNotEmpty) { return null; } final currentUserId = (AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim(); if (currentUserId.isEmpty || AccountStorage().getToken().trim().isEmpty) { return null; } final normalizedTargetRoomId = (targetRoomId ?? "").trim(); if (normalizedTargetRoomId.isNotEmpty && normalizedTargetRoomId == staleRoomId) { return null; } final staleRoomUserId = DataPersistence.getLastTimeRoomUserId().trim(); return _PersistedVoiceRoomCleanupRequest( staleRoomId: staleRoomId, targetRoomId: normalizedTargetRoomId, restoreOnlineHeartbeatWhenIdle: restoreOnlineHeartbeatWhenIdle, clearMarkerOnly: staleRoomUserId.isNotEmpty && staleRoomUserId != currentUserId, ); } void _startPersistedVoiceRoomCleanupInBackground( _PersistedVoiceRoomCleanupRequest? cleanupRequest, ) { unawaited(_runPersistedVoiceRoomCleanup(cleanupRequest)); } Future _runPersistedVoiceRoomCleanup( _PersistedVoiceRoomCleanupRequest? cleanupRequest, ) { if (cleanupRequest == null) { return Future.value(); } final runningTask = _persistedRoomCleanupTask; if (runningTask != null) { return runningTask; } final task = _cleanupPersistedVoiceRoomSessionBeforeEntry(cleanupRequest); _persistedRoomCleanupTask = task; return task.whenComplete(() { if (identical(_persistedRoomCleanupTask, task)) { _persistedRoomCleanupTask = null; } }); } Future _cleanupPersistedVoiceRoomSessionBeforeEntry( _PersistedVoiceRoomCleanupRequest cleanupRequest, ) async { final staleRoomId = cleanupRequest.staleRoomId; if (cleanupRequest.clearMarkerOnly) { await _clearPersistedRoomMarkerIfMatches(staleRoomId); return; } var didClean = false; await _retryExitNetworkRequest( 'quit persisted room $staleRoomId', () async { final didQuit = await SCAccountRepository().quitRoom( staleRoomId, silentErrorToast: true, ); if (!didQuit) { throw StateError('quitRoom returned false'); } didClean = true; }, ); if (didClean) { await _clearPersistedRoomMarkerIfMatches(staleRoomId); final currentRoomId = (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(); if (cleanupRequest.restoreOnlineHeartbeatWhenIdle && cleanupRequest.targetRoomId != staleRoomId && currentRoomId.isEmpty) { await SCHeartbeatUtils.scheduleHeartbeat( SCHeartbeatStatus.ONLINE.name, 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!); setVoiceRoomRouteVisible(true); } 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 roomRtcCredentialFuture = _captureStartupFuture( _fetchRoomRtcJoinCredentialForCurrentRoom(), ); final microphoneListFuture = retrieveMicrophoneList(); final onlineUsersFuture = fetchOnlineUsersList(); final joinResult = await joinGroupFuture; if (!_isActiveRoomStartup(entryRequestSerial, roomId)) { return; } if (joinResult == null || (joinResult.code != 0 && joinResult.code != 10013)) { _stopVoiceRoomForegroundService(); _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, ), ); unawaited( rtmProvider?.loadRoomHistoryMessages(groupId, count: 20) ?? Future.value(), ); _refreshRoomRedPacketListAfterEntry(roomId: roomId); await Future.wait([microphoneListFuture, onlineUsersFuture]); if (!_isActiveRoomStartup(entryRequestSerial, roomId)) { return; } _startRoomStatePolling(); socialChatRoomManager.fetchContributionLevelData(roomId); failureType = RoomStartupFailureType.rtc; final roomRtcCredentialResult = await roomRtcCredentialFuture; if (!_isActiveRoomStartup(entryRequestSerial, roomId)) { return; } final trtcUserSig = roomRtcCredentialResult.requireValue(); await joinRoomRtcVoiceChannel( throwOnError: true, trtcUserSig: trtcUserSig, ); if (!_isActiveRoomStartup(entryRequestSerial, roomId)) { await _cleanupRoomRtcState( clearMicSeats: currenRoom == null, onlyIfBusinessRoomId: roomId, ); return; } await _bootstrapRoomHeartbeatState(); _startRoomRedPacketPresenceHeartbeat(roomId); if (!_isActiveRoomStartup(entryRequestSerial, roomId)) { return; } _setRoomStartupReady(); _refreshRoomRedPacketListAfterEntry(roomId: roomId); final currentUserProfile = AccountStorage().getCurrentUser()?.userProfile; Msg joinMsg = Msg( groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "", user: currentUserProfile, msg: "", role: currenRoom?.entrants?.roles ?? "", type: SCRoomMsgType.joinRoom, ); rtmProvider?.dispatchMessage(joinMsg, addLocal: true); if (currentUserProfile != null) {} _playRoomEntryEffects( joinMsg, entryRequestSerial: entryRequestSerial, roomId: roomId, ); _loadRoomSecondaryData(); _scheduleRoomRocketEntryReadyFallback(roomId); } catch (error, stackTrace) { if (!_isActiveRoomStartup(entryRequestSerial, roomId)) { return; } _stopVoiceRoomForegroundService(); _setRoomStartupFailed(failureType); } } void announceCpRelationEntryForRoomUser({ required String groupId, required SocialChatUserProfile entrant, }) { final roomId = currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ""; if (roomId.isEmpty) { return; } unawaited( _announceCpRelationEntryIfNeeded( roomId: roomId, groupId: groupId, entrant: entrant, ), ); } Future _announceCpRelationEntryIfNeeded({ required String roomId, required String groupId, required SocialChatUserProfile entrant, }) async { final currentProfile = AccountStorage().getCurrentUser()?.userProfile; final normalizedGroupId = groupId.trim().isNotEmpty ? groupId.trim() : (currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "").trim(); if (currentProfile == null || normalizedGroupId.isEmpty) { return; } if (_isSameRelationProfile(currentProfile, entrant)) { return; } final entrantKey = _relationProfileKey(entrant); final ownerKey = _relationProfileKey(currentProfile); if (entrantKey.isEmpty || ownerKey.isEmpty) { return; } final notifiedKeys = {}; for (final relationType in const ["CP", "BROTHER", "SISTERS"]) { if (currenRoom?.roomProfile?.roomProfile?.id != roomId) { return; } final relations = await _safeFetchCurrentUserCpRelations(relationType); for (final relation in relations) { if (!_relationContainsProfile(relation, currentProfile) || !_relationContainsProfile(relation, entrant)) { continue; } final normalizedType = scNormalizeCpRelationType( relation.relationType ?? relationType, ); final notifyKey = "$normalizedGroupId:$normalizedType:$ownerKey:$entrantKey"; if (!notifiedKeys.add(notifyKey) || !_shouldEmitRelationEntryNotice(notifyKey)) { continue; } debugPrint( "[CP][RoomEntry] emit relation joined notice " "type=$normalizedType owner=$ownerKey entrant=$entrantKey", ); final entrantName = _roomRelationDisplayName(entrant); rtmProvider?.dispatchMessage( Msg( groupId: normalizedGroupId, msg: scBuildCpRelationJoinNotice( ownerName: _roomRelationDisplayName(currentProfile), joinedName: entrantName, relationType: normalizedType, ), type: SCRoomMsgType.cpSystemNotice, noticeAction: "RELATION_JOINED_ROOM", user: entrant, toUser: currentProfile, role: normalizedType, ), addLocal: true, ); } } } Future> _safeFetchCurrentUserCpRelations( String relationType, ) async { try { return await SCAccountRepository().cpRelationshipPairs( relationType: relationType, ); } catch (error) { debugPrint( "[CP][RoomEntry] pair failed relationType=$relationType error=$error", ); return const []; } } bool _relationContainsProfile(CPRes relation, SocialChatUserProfile profile) { final profileKeys = _relationProfileKeys(profile); if (profileKeys.isEmpty) { return false; } return _relationKeys(relation).any(profileKeys.contains); } bool _isSameRelationProfile( SocialChatUserProfile left, SocialChatUserProfile right, ) { final rightKeys = _relationProfileKeys(right); return _relationProfileKeys(left).any(rightKeys.contains); } Set _relationProfileKeys(SocialChatUserProfile profile) { return { profile.id?.trim() ?? "", profile.account?.trim() ?? "", profile.getID().trim(), }..removeWhere((value) => value.isEmpty); } Set _relationKeys(CPRes relation) { return { relation.meUserId?.trim() ?? "", relation.cpUserId?.trim() ?? "", relation.meAccount?.trim() ?? "", relation.cpAccount?.trim() ?? "", }..removeWhere((value) => value.isEmpty); } String _relationProfileKey(SocialChatUserProfile profile) { final keys = _relationProfileKeys(profile); return keys.isEmpty ? "" : keys.first; } bool _shouldEmitRelationEntryNotice(String key) { final now = DateTime.now().millisecondsSinceEpoch; _relationEntryNoticeAt.removeWhere((_, value) => now - value > 60000); final last = _relationEntryNoticeAt[key]; if (last != null && now - last < 30000) { debugPrint("[CP][RoomEntry] skip duplicate notice key=$key"); return false; } _relationEntryNoticeAt[key] = now; return true; } String _roomRelationDisplayName(SocialChatUserProfile profile) { return _firstNonBlankEntryResource([ profile.userNickname, profile.account, profile.id, "User", ]); } String _firstNonBlankEntryResource(Iterable values) { for (final value in values) { final text = value?.trim(); if (text != null && text.isNotEmpty) { return text; } } return ""; } void _playRoomEntryEffects( Msg joinMsg, { required int entryRequestSerial, required String roomId, }) { final entryResource = AccountStorage().getCurrentUser()?.userProfile?.getMountains(); if (entryResource != null) { final sourceUrl = entryResource.sourceUrl ?? ""; if (sourceUrl.isNotEmpty && SCGlobalConfig.allowsHighCostAnimations && SCGlobalConfig.isEntryVehicleAnimation && 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() { final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? ""; SCChatRoomRepository() .rocketStatus(roomId) .then((res) { roomRocketStatus = res; notifyListeners(); if (!res.isEnabled) { return; } _scheduleRoomRocketAssetPreload(roomId, res, afterRoomEntry: true); _scheduleRoomRocketEntryReadyFallback(roomId); _loadRoomRocketRewardPopups(roomId); }) .catchError((e) {}); _refreshRoomRedPacketListAfterEntry(); fetchRoomTaskClaimableCount(); } void _loadRoomRocketRewardPopups(String roomId) { final normalizedRoomId = roomId.trim(); if (normalizedRoomId.isEmpty || normalizedRoomId != (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim()) { return; } rtmProvider?.queryVoiceRoomRocketRewardPopups(normalizedRoomId); } ///更新房间火箭信息 void updateRoomRocketConfigurationStatus(SCRoomRocketStatusRes res) { roomRocketStatus = res; notifyListeners(); if (!res.isEnabled) { return; } _scheduleRoomRocketAssetPreload( (res.roomId ?? currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(), res, ); } ///刷新房间火箭信息 Future refreshRoomRocketStatus({String? roomId}) async { final resolvedRoomId = (roomId ?? currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(); if (resolvedRoomId.isEmpty) { return; } final res = await SCChatRoomRepository().rocketStatus(resolvedRoomId); if (resolvedRoomId != (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim()) { return; } roomRocketStatus = res; notifyListeners(); if (!res.isEnabled) { return; } _scheduleRoomRocketAssetPreload(resolvedRoomId, res); } void updateRoomRocketStatusFromRealtime( SCRoomRocketStatusRes update, { String? roomId, }) { final resolvedRoomId = (roomId ?? update.roomId ?? currenRoom?.roomProfile?.roomProfile?.id ?? "") .trim(); if (resolvedRoomId.isEmpty || resolvedRoomId != (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim()) { return; } if (!_hasRoomRocketRealtimeStatusFields(update)) { return; } final merged = roomRocketStatus?.mergeRealtimeUpdate(update, roomId: resolvedRoomId) ?? update; roomRocketStatus = merged; notifyListeners(); if (!merged.isEnabled) { return; } _scheduleRoomRocketAssetPreload(resolvedRoomId, merged); } bool _hasRoomRocketRealtimeStatusFields(SCRoomRocketStatusRes update) { return update.currentLevel != null || update.level != null || update.currentEnergy != null || update.maxEnergy != null || update.displayPercent != null || update.energyPercent != null || (update.levels?.isNotEmpty ?? false); } void _scheduleRoomRocketAssetPreload( String roomId, SCRoomRocketStatusRes status, { bool afterRoomEntry = false, }) { final resolvedRoomId = roomId.trim(); if (resolvedRoomId.isEmpty) { return; } if (!status.isEnabled) { return; } if (afterRoomEntry) { RoomRocketAssetPreloader.preloadAfterRoomEntry( roomId: resolvedRoomId, status: status, ); } else { RoomRocketAssetPreloader.preloadForStatus( roomId: resolvedRoomId, status: status, ); } if (RoomRocketAssetPreloader.shouldForceCurrentLaunchPreload(status)) { RoomRocketAssetPreloader.forcePreloadCurrentLaunchAssets( roomId: resolvedRoomId, status: status, ); } } void scheduleRoomRocketStatusRefreshForGift({String? roomId}) { final resolvedRoomId = (roomId ?? currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(); if (resolvedRoomId.isEmpty) { return; } _cancelRoomRocketGiftStatusRefresh(); for (final delay in _roomRocketGiftRefreshDelays) { final timer = Timer(delay, () { _roomRocketGiftRefreshTimers.removeWhere((item) => !item.isActive); if (resolvedRoomId != (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim()) { return; } unawaited(refreshRoomRocketStatus(roomId: resolvedRoomId)); }); _roomRocketGiftRefreshTimers.add(timer); } } void _cancelRoomRocketGiftStatusRefresh() { for (final timer in _roomRocketGiftRefreshTimers) { timer.cancel(); } _roomRocketGiftRefreshTimers.clear(); } void _scheduleRoomRocketEntryReadyFallback(String roomId) { final normalizedRoomId = roomId.trim(); if (normalizedRoomId.isEmpty) { return; } _notifyRoomRocketEntryReadyIfCurrent(normalizedRoomId); for (final delay in _roomRocketEntryReadyFallbackDelays) { final timer = Timer(delay, () { _roomRocketEntryReadyFallbackTimers.removeWhere( (item) => !item.isActive, ); _notifyRoomRocketEntryReadyIfCurrent(normalizedRoomId); }); _roomRocketEntryReadyFallbackTimers.add(timer); } } void _notifyRoomRocketEntryReadyIfCurrent(String roomId) { final normalizedRoomId = roomId.trim(); final currentRoomId = currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ""; if (normalizedRoomId.isEmpty || currentRoomId != normalizedRoomId || !_voiceRoomRouteVisible) { return; } rtmProvider?.handleVoiceRoomReadyForRocketRewards(normalizedRoomId); } void _cancelRoomRocketEntryReadyFallbacks() { for (final timer in _roomRocketEntryReadyFallbackTimers) { timer.cancel(); } _roomRocketEntryReadyFallbackTimers.clear(); } void scheduleRoomRocketPostLaunchStatusRefresh({String? roomId}) { final resolvedRoomId = (roomId ?? currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(); if (resolvedRoomId.isEmpty) { return; } _cancelRoomRocketPostLaunchStatusRefresh(); for (final delay in _roomRocketPostLaunchRefreshDelays) { final timer = Timer(delay, () { if (resolvedRoomId != (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim()) { return; } unawaited(refreshRoomRocketStatus(roomId: resolvedRoomId)); }); _roomRocketPostLaunchRefreshTimers.add(timer); } } void _cancelRoomRocketPostLaunchStatusRefresh() { for (final timer in _roomRocketPostLaunchRefreshTimers) { timer.cancel(); } _roomRocketPostLaunchRefreshTimers.clear(); } void advanceRoomRocketStatusAfterLaunch( RoomRocketLaunchBroadcastMessage launch, ) { final currentRoomId = (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(); if (!launch.isValid || launch.roomId != currentRoomId) { return; } final currentStatus = roomRocketStatus; if (currentStatus == null || !currentStatus.isEnabled) { return; } final currentLevel = (currentStatus.currentLevel ?? currentStatus.level?.toInt() ?? 1) .clamp(1, 99) .toInt(); final currentPercent = _roomRocketStatusPercent(currentStatus); if (currentStatus.currentEnergy == 0 && currentPercent <= 0 && currentLevel >= launch.safeLevel) { return; } final launchedLevel = math.max(currentLevel, launch.safeLevel); final nextLevel = _nextRoomRocketLevelAfterLaunch(launchedLevel); final nextLevelInfo = _roomRocketLevelFor(nextLevel); final update = SCRoomRocketStatusRes( roomId: launch.roomId, level: nextLevel, currentLevel: nextLevel, currentEnergy: 0, maxEnergy: nextLevelInfo?.needEnergy, needEnergy: nextLevelInfo?.needEnergy, energyPercent: 0, displayPercent: 0, configured: currentStatus.configured, enabled: currentStatus.enabled, levels: currentStatus.levels, ); roomRocketStatus = currentStatus.mergeRealtimeUpdate( update, roomId: launch.roomId, ); notifyListeners(); } int _nextRoomRocketLevelAfterLaunch(int launchedLevel) { final normalizedLevel = launchedLevel.clamp(1, 99).toInt(); final levels = [ ...(roomRocketStatus?.levels ?? const []), ]..sort((a, b) { final sortCompare = a.sort.compareTo(b.sort); return sortCompare != 0 ? sortCompare : a.level.compareTo(b.level); }); for (final level in levels) { if (level.enabled && level.level > normalizedLevel) { return level.level.clamp(1, 99).toInt(); } } return normalizedLevel; } double _roomRocketStatusPercent(SCRoomRocketStatusRes status) { final value = status.displayPercent ?? status.energyPercent; if (value != null) { final raw = value.toDouble(); final percent = raw > 0 && raw <= 1 ? raw * 100 : raw; return percent.clamp(0, 100).toDouble(); } final currentEnergy = status.currentEnergy; final maxEnergy = status.maxEnergy ?? status.needEnergy; if (currentEnergy == null || maxEnergy == null || maxEnergy <= 0) { return 0; } return ((currentEnergy / maxEnergy) * 100).clamp(0, 100).toDouble(); } SCRoomRocketLevelRes? _roomRocketLevelFor(int level) { final normalizedLevel = level.clamp(1, 99).toInt(); for (final item in roomRocketStatus?.levels ?? const []) { if (item.level == normalizedLevel) { return item; } } return null; } void handleRoomRocketLaunchBroadcast( RoomRocketLaunchBroadcastMessage launch, ) { final currentRoomId = (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(); if (!launch.isValid || launch.roomId != currentRoomId) { return; } _roomRocketLaunchAnimationTimer?.cancel(); _releaseRoomRocketLaunchTopAvatarNotifier(); if (shouldShowRoomVisualEffects && isVoiceRoomRouteVisible && !SCRoomTopLayerGuard().isActive) { final animationUrl = _firstNonBlankText([ launch.rocketAnimationUrl, _rocketLaunchAnimationUrlForLevel(launch.safeLevel), ]) ?? ""; if (animationUrl.isNotEmpty) { _playRoomRocketLaunchAnimation( animationUrl: animationUrl, launch: launch, ); } _roomRocketLaunchAnimationTimer = Timer(launch.displayDuration, () { _roomRocketLaunchAnimationTimer = null; SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag); _releaseRoomRocketLaunchTopAvatarNotifier(); unawaited(refreshRoomRocketStatus(roomId: launch.roomId)); scheduleRoomRocketPostLaunchStatusRefresh(roomId: launch.roomId); }); return; } unawaited(refreshRoomRocketStatus(roomId: launch.roomId)); scheduleRoomRocketPostLaunchStatusRefresh(roomId: launch.roomId); } String? _rocketLaunchAnimationUrlForLevel(int level) { final levels = roomRocketStatus?.levels ?? const []; for (final item in levels) { if (item.level == level && item.rocketAnimationUrl.trim().isNotEmpty) { return item.rocketAnimationUrl; } } return null; } void _playRoomRocketLaunchAnimation({ required String animationUrl, required RoomRocketLaunchBroadcastMessage launch, }) { if (SCRoomTopLayerGuard().isActive) { return; } if (RoomRocketPagEffectOverlay.isPag(animationUrl)) { final topAvatarNotifier = ValueNotifier( _roomRocketTop1AvatarUrl(launch.safeLevel), ); _roomRocketLaunchTopAvatarNotifier = topAvatarNotifier; unawaited(_loadRoomRocketTop1Avatar(launch, topAvatarNotifier)); SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag); SmartDialog.show( tag: _roomRocketPagLaunchDialogTag, alignment: Alignment.center, debounce: true, animationType: SmartAnimationType.fade, maskColor: Colors.transparent, clickMaskDismiss: false, builder: (_) => RoomRocketPagEffectOverlay( resource: animationUrl, topAvatarUrlListenable: topAvatarNotifier, onCompleted: () { SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag); _roomRocketLaunchAnimationTimer?.cancel(); _roomRocketLaunchAnimationTimer = null; _releaseRoomRocketLaunchTopAvatarNotifier(topAvatarNotifier); unawaited(refreshRoomRocketStatus(roomId: launch.roomId)); scheduleRoomRocketPostLaunchStatusRefresh( roomId: launch.roomId, ); }, ), ); return; } SCGiftVapSvgaManager().play( animationUrl, priority: SCGiftVapSvgaManager.entryEffectPriority + 100, type: SCGiftVapSvgaManager.entryEffectType, ); } String? _roomRocketTop1AvatarUrl(int level) { final kings = roomRocketStatus?.rocketKingsForLevel(level) ?? const []; return _roomRocketTop1AvatarUrlFromUsers(kings); } String? _roomRocketTop1AvatarUrlFromKing(SCRoomRocketKingRes king) { final source = king.podium.isNotEmpty ? king.podium : king.displayRecords; return _roomRocketTop1AvatarUrlFromUsers(source); } String? _roomRocketTop1AvatarUrlFromUsers( List kings, ) { if (kings.isEmpty) { return null; } for (final item in kings) { if (item.rank == 1 && item.userAvatar.trim().isNotEmpty) { return item.userAvatar; } } for (final item in kings) { if (item.userAvatar.trim().isNotEmpty) { return item.userAvatar; } } return null; } Future _loadRoomRocketTop1Avatar( RoomRocketLaunchBroadcastMessage launch, ValueNotifier notifier, ) async { final roomId = launch.roomId.trim(); if (roomId.isEmpty) { return; } try { _roomRocketDebug( 'request launch top1 avatar roomId=$roomId ' 'level=${launch.safeLevel} roundNo=${launch.roundNo}', ); var king = await SCChatRoomRepository().roomRocketKing( roomId, level: launch.safeLevel, roundNo: launch.roundNo, cursor: 1, limit: 3, ); var avatar = _roomRocketTop1AvatarUrlFromKing(king); if (avatar == null && launch.roundNo > 0) { _roomRocketDebug( 'launch top1 avatar retry without roundNo ' 'level=${launch.safeLevel} roundNo=${launch.roundNo}', ); king = await SCChatRoomRepository().roomRocketKing( roomId, level: launch.safeLevel, cursor: 1, limit: 3, ); avatar = _roomRocketTop1AvatarUrlFromKing(king); } if (_roomRocketLaunchTopAvatarNotifier != notifier) { return; } _roomRocketDebug( 'launch top1 avatar loaded level=${launch.safeLevel} ' 'roundNo=${launch.roundNo} avatar=${_shortRoomRocketDebugUrl(avatar)} ' 'podium=${king.podium.length} records=${king.records.length}', ); if (avatar != null && avatar != notifier.value) { notifier.value = avatar; } } catch (error) { _roomRocketDebug( 'launch top1 avatar load failed level=${launch.safeLevel} ' 'roundNo=${launch.roundNo}: $error', ); } } void _releaseRoomRocketLaunchTopAvatarNotifier([ ValueNotifier? expected, ]) { final notifier = _roomRocketLaunchTopAvatarNotifier; if (notifier == null || (expected != null && notifier != expected)) { return; } _roomRocketLaunchTopAvatarNotifier = null; WidgetsBinding.instance.addPostFrameCallback((_) { notifier.dispose(); }); } void _roomRocketDebug(String message) { if (!kDebugMode) { return; } debugPrint('[RoomRocket] $message'); } String _shortRoomRocketDebugUrl(String? url) { final value = url?.trim() ?? ''; if (value.isEmpty) { return '-'; } if (value.length <= 96) { return value; } return '${value.substring(0, 48)}...${value.substring(value.length - 24)}'; } RoomRocketRewardUserProfile? roomRocketRewardUserProfileForRecord( SCRoomRocketRewardRecordRes record, ) { final keys = { record.userId.trim(), record.account.trim(), }.where((item) => item.isNotEmpty).toSet(); if (keys.isEmpty) { return null; } for (final profile in _roomRocketKnownUserProfiles()) { if (_roomRocketProfileMatches(profile, keys)) { return RoomRocketRewardUserProfile( userId: profile.id?.trim() ?? '', account: profile.account?.trim() ?? '', nickname: profile.userNickname?.trim() ?? '', avatarUrl: profile.userAvatar?.trim() ?? '', ); } } return null; } Iterable _roomRocketKnownUserProfiles() sync* { final currentUser = AccountStorage().getCurrentUser()?.userProfile; if (currentUser != null) { yield currentUser; } final ownerProfile = currenRoom?.roomProfile?.userProfile; if (ownerProfile != null) { yield ownerProfile; } for (final user in onlineUsers) { yield user; } for (final seat in roomWheatMap.values) { final user = seat.user; if (user != null) { yield user; } } for (final user in SCRoomUtils.roomUsersMap.values) { yield user; } } bool _roomRocketProfileMatches( SocialChatUserProfile profile, Set keys, ) { return [profile.id, profile.account, profile.inRoomId].any((value) { final text = value?.trim() ?? ''; return text.isNotEmpty && keys.contains(text); }); } ///获取在线用户 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; } else {} } 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; } _applyMicListSnapshot( roomWheatList, notifyIfUnchanged: notifyIfUnchanged, ); } catch (error) { if (_isMissingPathError(error)) { _disableMicListRefreshForCurrentSession = true; _deferredMicListRefreshTimer?.cancel(); _deferredMicListRefreshTimer = null; if (notifyIfUnchanged) { notifyListeners(); } } else {} } 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 { final roomId = currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ""; if (roomId.isEmpty) { return const []; } var result = await SCChatRoomRepository().roomRedPacketList( roomId, current, ); final currentRoomId = currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ""; if (current == 1 && currentRoomId == roomId) { redPacketList = result; notifyListeners(); } return result; } void sendRoomTextMessage(String message, {String? dedupKey}) { final normalizedMessage = message.trim(); final normalizedDedupKey = dedupKey?.trim() ?? ""; final groupId = currenRoom?.roomProfile?.roomProfile?.roomAccount ?? ""; if (normalizedMessage.isEmpty || groupId.isEmpty) { return; } unawaited( rtmProvider?.dispatchMessage( Msg( groupId: groupId, msg: normalizedMessage, type: SCRoomMsgType.text, role: currenRoom?.entrants?.roles ?? "", giftBatchId: normalizedDedupKey.isEmpty ? null : normalizedDedupKey, user: AccountStorage().getCurrentUser()?.userProfile, ), ) ?? Future.value(), ); } void sendRoomRedPacketClaimMessage({ required String packetId, required num amount, required String packetSenderName, }) { final groupId = currenRoom?.roomProfile?.roomProfile?.roomAccount ?? ""; if (groupId.isEmpty) { return; } final normalizedPacketId = packetId.trim(); final normalizedSenderName = packetSenderName.trim(); final currentUser = AccountStorage().getCurrentUser()?.userProfile; final currentUserId = (currentUser?.id ?? "").trim(); final dedupKey = normalizedPacketId.isNotEmpty && currentUserId.isNotEmpty ? 'room-red-packet-claim-$normalizedPacketId-$currentUserId' : null; unawaited( rtmProvider?.dispatchMessage( Msg( groupId: groupId, msg: normalizedSenderName, type: SCRoomMsgType.roomRedPacketClaim, role: currenRoom?.entrants?.roles ?? "", number: amount, giftBatchId: dedupKey, user: currentUser, ), ) ?? Future.value(), ); } void _refreshRoomRedPacketListAfterEntry({String? roomId}) { final normalizedRoomId = roomId?.trim() ?? currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ""; if (normalizedRoomId.isEmpty) { return; } for (final delay in _roomRedPacketEntryRefreshDelays) { unawaited( Future.delayed(delay).then((_) { return _refreshRoomRedPacketListIfCurrent(normalizedRoomId); }), ); } } Future _refreshRoomRedPacketListIfCurrent(String roomId) async { final currentRoomId = currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ""; if (roomId.trim().isEmpty || currentRoomId != roomId.trim()) { return; } try { await loadRoomRedPacketList(1); } catch (error) {} } void _startRoomRedPacketPresenceHeartbeat(String roomId) { final normalizedRoomId = roomId.trim(); _stopRoomRedPacketPresenceHeartbeat(); if (normalizedRoomId.isEmpty) { return; } void sendHeartbeat() { if (!_isCurrentRoomId(normalizedRoomId)) { _stopRoomRedPacketPresenceHeartbeat(); return; } SCChatRoomRepository() .roomRedPacketPresenceHeartbeat(normalizedRoomId) .catchError((e) { return SCRoomRedPacketPresenceRes(); }); } sendHeartbeat(); _roomRedPacketPresenceTimer = Timer.periodic( const Duration(seconds: 30), (_) => sendHeartbeat(), ); } void _stopRoomRedPacketPresenceHeartbeat() { if (_roomRedPacketPresenceTimer != null) { _roomRedPacketPresenceTimer?.cancel(); } _roomRedPacketPresenceTimer = null; } 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 { SCGiftVapSvgaManager().stopPlayback(); setRoomVisualEffectsEnabled(false); SCHeartbeatUtils.cancelTimer(); _stopRoomRedPacketPresenceHeartbeat(); exitRtmProvider?.cleanRoomData(); } catch (e) {} await _notifyRoomMusicSessionExit(); _clearData(clearPersistedRoomMarker: false); if (!isLogout && navigationContext != null && navigationContext.mounted) { VoiceRoomRoute.popVoiceRoomToPrevious(navigationContext); } final roomRtcLeaveTask = _leaveRoomRtcForExit(); final remoteCleanupTask = _cleanupExitedRoomRemotely( exitRtmProvider: exitRtmProvider, groupId: isPreviewOnlyExit ? "" : groupId, roomId: isPreviewOnlyExit ? "" : roomId, shouldSendOnlineHeartbeat: !isLogout, ); if (deferRemoteCleanup) { unawaited(roomRtcLeaveTask); unawaited(remoteCleanupTask); } else { await roomRtcLeaveTask; await remoteCleanupTask; } } Future _switchAwayFromCurrentRoomForNewEntry() async { _stopRoomStatePolling(); final groupId = currenRoom?.roomProfile?.roomProfile?.roomAccount ?? ""; final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? ""; final isPreviewOnlyExit = _currentRoomIsEntryPreview; final exitRtmProvider = rtmProvider; try { SCGiftVapSvgaManager().stopPlayback(); setRoomVisualEffectsEnabled(false); SCHeartbeatUtils.cancelTimer(); _stopRoomRedPacketPresenceHeartbeat(); exitRtmProvider?.cleanRoomData(); } catch (e) {} await _notifyRoomMusicSessionExit(); _clearData(clearPersistedRoomMarker: false); final roomRtcLeaveTask = _leaveRoomRtcForExit(); _pendingRoomSwitchRtcLeaveTask = roomRtcLeaveTask; unawaited( roomRtcLeaveTask.whenComplete(() { if (identical(_pendingRoomSwitchRtcLeaveTask, roomRtcLeaveTask)) { _pendingRoomSwitchRtcLeaveTask = 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) { 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)) { return; } await exitRtmProvider.quitGroup(groupId); }), ); } if (roomId.isNotEmpty) { tasks.add( _retryExitNetworkRequest( 'leave room red packet presence $roomId', () async { await SCChatRoomRepository().roomRedPacketPresenceLeave(roomId); }, ), ); tasks.add( _retryExitNetworkRequest('quit room $roomId', () async { if (_isCurrentRoomId(roomId)) { return; } final didQuit = await SCAccountRepository().quitRoom( roomId, silentErrorToast: true, ); if (!didQuit) { throw StateError('quitRoom returned false'); } await _clearPersistedRoomMarkerIfMatches(roomId); }), ); } await Future.wait(tasks); } Future _clearPersistedRoomMarkerIfMatches(String roomId) async { final normalizedRoomId = roomId.trim(); if (normalizedRoomId.isEmpty) { return; } if (DataPersistence.getLastTimeRoomId().trim() != normalizedRoomId) { return; } await Future.wait([ DataPersistence.setLastTimeRoomId(""), DataPersistence.setLastTimeRoomUserId(""), ]); } 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; if (attempt < _exitNetworkRetryLimit) { await Future.delayed(_exitNetworkRetryDelay * attempt); } } } } 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 _cleanupRoomRtcState(); await Future.delayed(const Duration(milliseconds: 100)); } catch (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(); _stopRoomRedPacketPresenceHeartbeat(); rtmProvider?.cleanRoomData(); if (!isPreviewOnlyFailure && groupId.isNotEmpty) { await rtmProvider?.quitGroup(groupId); } await _cleanupRoomRtcState(clearMicSeats: true); await Future.delayed(const Duration(milliseconds: 100)); if (!isPreviewOnlyFailure && roomId.isNotEmpty) { await SCChatRoomRepository().roomRedPacketPresenceLeave(roomId); final didQuit = await SCAccountRepository().quitRoom( roomId, silentErrorToast: true, ); if (didQuit) { await _clearPersistedRoomMarkerIfMatches(roomId); } } } catch (e) { } finally { _clearData(); _isHandlingRoomStartupFailure = false; notifyListeners(); if (navigator != null && navigator.mounted && navigator.canPop()) { navigator.pop(); } } } ///清空列表数据 void _clearData({bool clearPersistedRoomMarker = true}) { _roomEntryRequestSerial += 1; _stopVoiceRoomForegroundService(); _roomEntryEffectTimer?.cancel(); _roomEntryEffectTimer = null; _roomRocketLaunchAnimationTimer?.cancel(); _roomRocketLaunchAnimationTimer = null; SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag); _releaseRoomRocketLaunchTopAvatarNotifier(); _cancelRoomRocketGiftStatusRefresh(); _cancelRoomRocketPostLaunchStatusRefresh(); RoomRocketAssetPreloader.cancelCurrentRoom(); _stopRoomRedPacketPresenceHeartbeat(); _previewRoomSeatCount = null; _finishRoomStartupSeatLoading(executePendingAction: false); _resetSelfMicGoUpFailureState(); _clearRoomContributionLevelData(); _stopRoomStatePolling(); _resetHeartbeatTracking(); _resetRoomRtcTracking(); _disableMicListRefreshForCurrentSession = false; _disableOnlineUsersRefreshForCurrentSession = false; _isSelfMicActionInFlight = false; _roomStartupStatus = RoomStartupStatus.idle; _roomStartupFailureType = RoomStartupFailureType.none; _currentRoomIsEntryPreview = false; _isHandlingRoomStartupFailure = false; _isHandlingRoomRtcFailure = false; _pendingMicListRefresh = false; _pendingMicListRefreshNotifyIfUnchanged = false; _recentMicChangeSeatSnapshots.clear(); _seatEmojiEventVersions.clear(); _emojiPlaybackEventVersion = 0; _roomMicRelationCache.clear(); _roomMicRelationLoadingUserIds.clear(); _roomMicRelationDismissedPairUntil.clear(); _clearRoomMusicPublishingStates(); roomRocketStatus = null; rtmProvider ?.onNewMessageListenerGroupMap["${currenRoom?.roomProfile?.roomProfile?.roomAccount}"] = null; roomWheatMap.clear(); onlineUsers.clear(); managerUsers.clear(); needUpDataUserInfo = false; SCRoomUtils.roomUsersMap.clear(); _clearSelfMicSwitchGuard(clearPreferredIndex: true); _clearSelfMicGoUpGuard(clearPreferredIndex: true); _clearSelfMicReleaseGuard(); _seatResizeOccupantPreserveUntilMs = null; roomIsMute = false; rtmProvider?.roomAllMsgList.clear(); rtmProvider?.roomChatMsgList.clear(); redPacketList.clear(); currenRoom = null; _roomVisualEffectsEnabled = false; _voiceRoomRouteVisible = false; _isExitingCurrentVoiceRoomSession = false; isMic = true; isMusicPlaying = false; if (clearPersistedRoomMarker) { DataPersistence.setLastTimeRoomId(""); DataPersistence.setLastTimeRoomUserId(""); } SCGiftVapSvgaManager().stopPlayback(); SCRoomUtils.closeAllDialogs(); SCGlobalConfig.resetVisualEffectSwitchesToRecommendedDefaults(); } void _clearRoomContributionLevelData([BuildContext? targetContext]) { final currentContext = targetContext ?? context; resolveSocialChatRoomManager(currentContext).clearContributionLevelData(); } @override void dispose() { _stopRoomStatePolling(); _cancelRoomRocketGiftStatusRefresh(); _cancelRoomRocketPostLaunchStatusRefresh(); _cancelRoomRocketEntryReadyFallbacks(); _clearRoomMusicPublishingStates(); unawaited(releaseRtcEngineForAppTermination()); super.dispose(); } void toggleRemoteAudioMuteForAllUsers() { roomIsMute = !roomIsMute; _roomRtcEngineAdapter?.muteAllRemoteAudio(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) { ///是自己,直接打开资料卡 RoomUserInfoCard.showPreloaded(context!, 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) { return true; } 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; } RoomUserInfoCard.showPreloaded(context!, userId: normalizedUserId); } addSoundVoiceChangeListener(OnSoundVoiceChange onSoundVoiceChange) { _onSoundVoiceChangeList.add(onSoundVoiceChange); } removeSoundVoiceChangeListener(OnSoundVoiceChange onSoundVoiceChange) { _onSoundVoiceChangeList.remove(onSoundVoiceChange); } 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 selfMicMuted = micGoUpRes.micMute ?? false; final switched = await _switchRoomRtcToBroadcaster( muted: selfMicMuted, keepAudioPublishing: selfMicMuted && _currentUserPublishingRoomMusic, ); if (!switched) { await _switchRoomRtcToAudience(); try { await SCChatRoomRepository().micGoDown( currenRoom?.roomProfile?.roomProfile?.id ?? "", targetIndex, ); } catch (downError) {} _clearSelfMicSwitchGuard(clearPreferredIndex: true); _clearSelfMicGoUpGuard(clearPreferredIndex: true); _clearUserFromSeats(myUser?.id); _syncSelfMicRuntimeState(); notifyListeners(); _refreshMicListSilently(notifyIfUnchanged: true); _handleSelfMicGoUpFailure( 'Failed to put on the microphone, TRTC 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 { _clearSelfMicSwitchGuard(); _startSelfMicGoUpGuard(targetIndex: targetIndex); } _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, ); } isMic = selfMicMuted; _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); _clearSelfMicGoUpGuard(clearPreferredIndex: true); if (currentUserSeatIndex > -1) { _startSelfMicReleaseGuard(sourceIndex: currentUserSeatIndex); } else { _clearSelfMicReleaseGuard(); } SCHeartbeatUtils.cancelAnchorTimer(); await _switchRoomRtcToAudience(); _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; } } ///踢人下麦 Future killXiaMai(String userId) async { try { final targetUserId = userId.trim(); final roomId = currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ""; if (targetUserId.isEmpty || roomId.isEmpty) { SCTts.show("kill the user fail"); return; } num? targetMicIndex; for (final entry in roomWheatMap.entries) { if ((entry.value.user?.id ?? "").trim() == targetUserId) { targetMicIndex = entry.key; break; } } if (targetMicIndex == null) { SCTts.show("cant kill the user"); return; } var canKill = true; try { canKill = await SCChatRoomRepository().kickOffMicrophone( roomId, targetUserId, mickIndex: targetMicIndex, ); } catch (error) { if (!_isInvalidKickOffMicPrecheckError(error)) { rethrow; } } if (canKill) { await SCChatRoomRepository().micKill(roomId, targetMicIndex); Provider.of(context!, listen: false).dispatchMessage( Msg( groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount, msg: targetUserId, type: SCRoomMsgType.killXiaMai, ), addLocal: false, ); roomWheatMap[targetMicIndex]?.setUser = null; notifyListeners(); } else { SCTts.show("cant kill the user"); } } catch (e) { SCTts.show("kill the user fail"); } } ///自己是否在麦上 bool isOnMai() { final currentUserKeys = _currentUserIdentityKeys(); if (currentUserKeys.isEmpty) { return false; } for (final entry in roomWheatMap.entries) { if (_userProfileMatchesAnyIdentity( micAtIndexForDisplay(entry.key)?.user, currentUserKeys, )) { return true; } } return false; } ///自己是否在指定的麦上 bool isOnMaiInIndex(num index) { return _userProfileMatchesAnyIdentity( micAtIndexForDisplay(index)?.user, _currentUserIdentityKeys(), ); } ///点击的用户在哪个麦上 num userOnMaiInIndex(String userId) { return _findUserOnMaiInIndex(userId, useDisplaySeatForCurrentUser: true); } num _findUserOnMaiInIndex( String userId, { required bool useDisplaySeatForCurrentUser, }) { final normalizedUserId = userId.trim(); if (normalizedUserId.isEmpty) { return -1; } num index = -1; final currentUserKeys = _currentUserIdentityKeys(); final isCurrentUserQuery = currentUserKeys.contains(normalizedUserId); final lookupKeys = {normalizedUserId}; if (isCurrentUserQuery) { lookupKeys.addAll(currentUserKeys); } lookupKeys.removeWhere((item) => item.isEmpty); roomWheatMap.forEach((k, value) { final candidateSeat = useDisplaySeatForCurrentUser && isCurrentUserQuery ? micAtIndexForDisplay(k) : value; if (_userProfileMatchesAnyIdentity(candidateSeat?.user, lookupKeys)) { 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; } final nextMap = _buildMicMap(mics); _rememberMicChangeSnapshot(mics); _applyMicSnapshot(nextMap); } ///找一个空麦位 -1表示没有空位 num findWheat() { final seatCount = _currentConfiguredRoomSeatCount(); if (seatCount != null) { for (var index = 0; index < seatCount; index += 1) { final seat = roomWheatMap[index]; if (seat == null || (seat.user == null && !(seat.micLock ?? false))) { return index; } } return -1; } 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 { final value = await SCChatRoomRepository().specific(roomId); final currentRoomProfile = currenRoom?.roomProfile?.roomProfile; final previousMikeSize = currenRoom?.roomProfile?.roomSetting?.mikeSize; final nextRoomSetting = value.setting ?? currenRoom?.roomProfile?.roomSetting; _markSeatResizeOccupantPreserveWindow( previousSeatCount: previousMikeSize, nextSeatCount: nextRoomSetting?.mikeSize, ); currenRoom = currenRoom?.copyWith( roomProfile: currenRoom?.roomProfile?.copyWith( roomCounter: value.counter ?? currenRoom?.roomProfile?.roomCounter, roomSetting: nextRoomSetting, 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, ), ), ), ); roomWheatMap = _withSeatPlaceholdersForCurrentSetting(roomWheatMap); ///如果是游客禁止上麦,已经在麦上的游客需要下麦 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 _switchRoomRtcToBroadcaster( muted: true, keepAudioPublishing: _currentUserPublishingRoomMusic, ); } 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 _switchRoomRtcToBroadcaster(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) { final previousMikeSize = currenRoom?.roomProfile?.roomSetting?.mikeSize; currenRoom?.roomProfile?.setRoomSetting(roomSetting); _markSeatResizeOccupantPreserveWindow( previousSeatCount: previousMikeSize, nextSeatCount: roomSetting.mikeSize, ); roomWheatMap = _withSeatPlaceholdersForCurrentSetting(roomWheatMap); 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; } unawaited( startRoomMusicAudioMixing( filePath: localPath, loopback: false, cycle: cycle, ).catchError((_) {}), ); } void setRoomMusicMixingStateListener(RoomMusicMixingStateListener? listener) { _roomMusicMixingStateListener = listener; } void setRoomMusicMicRouteChangedListener( RoomMusicMicRouteChangedListener? listener, ) { _roomMusicMicRouteChangedListener = listener; } void setRoomMusicSessionExitListener(RoomMusicSessionExitListener? listener) { _roomMusicSessionExitListener = listener; } Future _notifyRoomMusicSessionExit() async { final listener = _roomMusicSessionExitListener; if (listener == null) { return; } try { await listener(); } catch (_) {} } void updateRoomMusicPublishingState({ required String? userId, required bool publishing, bool notifyIfChanged = true, }) { final normalizedUserId = (userId ?? "").trim(); if (normalizedUserId.isEmpty) { return; } bool changed = false; if (publishing) { changed = _roomMusicPublishingUserIds.add(normalizedUserId); _roomMusicPublishingClearTimers.remove(normalizedUserId)?.cancel(); _roomMusicPublishingClearTimers[normalizedUserId] = Timer( _roomMusicPublishingStateTtl, () { _roomMusicPublishingClearTimers.remove(normalizedUserId); if (_roomMusicPublishingUserIds.remove(normalizedUserId)) { if (_isCurrentUserId(normalizedUserId)) { _syncSelfMicRuntimeState(); } notifyListeners(); } }, ); } else { _roomMusicPublishingClearTimers.remove(normalizedUserId)?.cancel(); changed = _roomMusicPublishingUserIds.remove(normalizedUserId); } if (changed && _isCurrentUserId(normalizedUserId)) { _syncSelfMicRuntimeState(); } if (changed && notifyIfChanged) { notifyListeners(); } } void _clearRoomMusicPublishingStates() { for (final timer in _roomMusicPublishingClearTimers.values) { timer.cancel(); } _roomMusicPublishingClearTimers.clear(); _roomMusicPublishingUserIds.clear(); } void handleRoomMusicPublishingMessage(Msg msg) { final userId = (msg.role ?? "").trim().isNotEmpty ? msg.role : msg.user?.id ?? ""; final statusText = (msg.msg ?? "").trim().toLowerCase(); final publishing = statusText == "1" || statusText == "true" || (msg.number ?? 0) > 0; updateRoomMusicPublishingState(userId: userId, publishing: publishing); } void publishCurrentUserRoomMusicState(bool publishing) { final currentUser = AccountStorage().getCurrentUser()?.userProfile; final currentUserId = (currentUser?.id ?? "").trim(); if (currentUserId.isEmpty) { return; } final effectivePublishing = publishing && isOnMai(); updateRoomMusicPublishingState( userId: currentUserId, publishing: effectivePublishing, ); final groupId = currenRoom?.roomProfile?.roomProfile?.roomAccount?.trim() ?? ""; if (groupId.isEmpty) { return; } if (rtmProvider == null && context != null && context!.mounted) { rtmProvider = Provider.of(context!, listen: false); } final roomRtmProvider = rtmProvider; unawaited( roomRtmProvider?.dispatchMessage( Msg( groupId: groupId, msg: effectivePublishing ? "1" : "0", type: SCRoomMsgType.roomMusic, role: currentUserId, number: effectivePublishing ? 1 : 0, user: currentUser, ), addLocal: false, ) ?? Future.value(), ); } void republishCurrentUserRoomMusicStateIfNeeded() { final currentUserId = (AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim(); if (isUserPublishingRoomMusic(currentUserId)) { publishCurrentUserRoomMusicState(true); } } Future startRoomMusicAudioMixing({ required String filePath, required bool loopback, int cycle = 1, int startPos = 0, }) async { if (filePath.trim().isEmpty) { return; } if (!loopback) { final canPublish = await _ensureRoomMusicPublishReady(); if (!canPublish) { throw StateError('Current user is not ready to publish room music'); } } await _currentRoomRtcEngineAdapter.startAudioMixing( RoomRtcAudioMixingConfig( filePath: filePath, loopback: loopback, cycle: cycle, startPos: startPos, ), ); } Future pauseRoomMusicAudioMixing() async { await _roomRtcEngineAdapter?.pauseAudioMixing(); } Future resumeRoomMusicAudioMixing() async { await _roomRtcEngineAdapter?.resumeAudioMixing(); } Future stopRoomMusicAudioMixing() async { await _roomRtcEngineAdapter?.stopAudioMixing(); } Future setRoomMusicAudioMixingPosition(int positionMs) async { await _roomRtcEngineAdapter?.setAudioMixingPosition(positionMs); } Future getRoomMusicAudioMixingPosition() async { return await _roomRtcEngineAdapter?.getAudioMixingPosition() ?? 0; } Future getRoomMusicAudioMixingDuration() async { return await _roomRtcEngineAdapter?.getAudioMixingDuration() ?? 0; } Future setRoomMusicAudioMixingVolume(int volume) async { final safeVolume = volume.clamp(0, 100).toInt(); await _roomRtcEngineAdapter?.setAudioMixingVolume(safeVolume); } Future _ensureRoomMusicPublishReady() async { if (!_roomRtcJoined || _roomRtcEngineAdapter == null) { return false; } final currentUserMic = _currentUserMicSeat(); if (currentUserMic == null) { return false; } return _switchRoomRtcToBroadcaster( muted: (currentUserMic.micMute ?? false) || isMic, keepAudioPublishing: true, ); } }