yumi-flutter/lib/services/audio/rtc_manager.dart
2026-06-02 19:57:41 +08:00

5982 lines
183 KiB
Dart

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 RtcProvider = RealTimeCommunicationManager;
enum RoomStartupStatus { idle, loading, ready, failed }
enum RoomStartupFailureType { none, entry, kicked, im, rtc }
enum _RoomRtcLocalAudioRole { audience, anchor }
class _RoomStartupAsyncResult<T> {
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<Duration> _roomRocketGiftRefreshDelays = [
Duration(milliseconds: 700),
Duration(milliseconds: 1600),
Duration(milliseconds: 3200),
];
static const List<Duration> _roomRocketPostLaunchRefreshDelays = [
Duration(milliseconds: 800),
Duration(seconds: 2),
Duration(seconds: 5),
Duration(seconds: 10),
];
static const List<Duration> _roomRocketEntryReadyFallbackDelays = [
Duration(milliseconds: 300),
Duration(milliseconds: 900),
Duration(milliseconds: 1800),
Duration(milliseconds: 3200),
];
static const List<String> _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<Duration> _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<String, int> _relationEntryNoticeAt = <String, int>{};
int? _previewRoomSeatCount;
bool _currentRoomIsEntryPreview = false;
bool _isHandlingRoomStartupFailure = false;
Timer? _micListPollingTimer;
Timer? _onlineUsersPollingTimer;
Timer? _roomEntryEffectTimer;
Timer? _roomRocketLaunchAnimationTimer;
ValueNotifier<String?>? _roomRocketLaunchTopAvatarNotifier;
final List<Timer> _roomRocketGiftRefreshTimers = [];
final List<Timer> _roomRocketPostLaunchRefreshTimers = [];
final List<Timer> _roomRocketEntryReadyFallbackTimers = [];
Timer? _roomRedPacketPresenceTimer;
Future<void>? _rtcEnginePrewarmTask;
Future<void>? _pendingRoomSwitchRtcLeaveTask;
Future<void>? _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<num, _RecentMicChangeSeatSnapshot> _recentMicChangeSeatSnapshots =
<num, _RecentMicChangeSeatSnapshot>{};
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<OnSoundVoiceChange> _onSoundVoiceChangeList = [];
///麦位
Map<num, MicRes> roomWheatMap = {};
final Map<num, int> _seatEmojiEventVersions = {};
int _emojiPlaybackEventVersion = 0;
final Map<String, _RoomMicRelationProfileCache> _roomMicRelationCache =
<String, _RoomMicRelationProfileCache>{};
final Set<String> _roomMicRelationLoadingUserIds = <String>{};
final Map<String, DateTime> _roomMicRelationDismissedPairUntil =
<String, DateTime>{};
final Set<String> _roomMusicPublishingUserIds = <String>{};
final Map<String, Timer> _roomMusicPublishingClearTimers = <String, Timer>{};
RoomRtcEngineAdapter? _roomRtcEngineAdapter;
RoomMusicMixingStateListener? _roomMusicMixingStateListener;
RoomMusicMicRouteChangedListener? _roomMusicMicRouteChangedListener;
BuildContext? context;
RtmProvider? rtmProvider;
SCIsFollowRoomRes? isFollowRoomRes;
SCRoomRocketStatusRes? roomRocketStatus;
///房间是否静音
bool roomIsMute = false;
bool closeFullGame = false;
///禁音开关 默认关闭
bool isMic = true;
bool shouldShowSeatSoundWave(num index) {
final seat = roomWheatMap[index];
final userId = (seat?.user?.id ?? "").trim();
if (userId.isEmpty || (seat?.micMute ?? false)) {
return false;
}
final currentUserId =
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
if (userId == currentUserId && isMic) {
return false;
}
return true;
}
///在线用户列表
List<SocialChatUserProfile> onlineUsers = [];
///房间管理员
List<SocialChatUserProfile> 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<String> _currentUserIdentityKeys() {
final currentUser = AccountStorage().getCurrentUser()?.userProfile;
return {currentUser?.id?.trim() ?? "", currentUser?.account?.trim() ?? ""}
..removeWhere((item) => item.isEmpty);
}
Set<String> _userProfileIdentityKeys(SocialChatUserProfile? user) {
return {user?.id?.trim() ?? "", user?.account?.trim() ?? ""}
..removeWhere((item) => item.isEmpty);
}
bool _userProfileMatchesAnyIdentity(
SocialChatUserProfile? user,
Set<String> identityKeys,
) {
if (identityKeys.isEmpty) {
return false;
}
return _userProfileIdentityKeys(user).any(identityKeys.contains);
}
///房间红包列表
List<SCRoomRedPacketListRes> 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<void>.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<SocialChatUserProfile> previous,
List<SocialChatUserProfile> 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<num, MicRes> previous, Map<num, MicRes> 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<SocialChatUserProfile> 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<num, MicRes> _withSeatPlaceholdersForCurrentSetting(
Map<num, MicRes> source,
) {
final seatCount = _currentConfiguredRoomSeatCount();
if (seatCount == null) {
return source;
}
final roomId = currenRoom?.roomProfile?.roomProfile?.id;
final nextMap = Map<num, MicRes>.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<num, MicRes> _preserveMicOccupantsDuringSeatResize(
Map<num, MicRes> nextMap, {
required Map<num, MicRes> 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<num, MicRes>.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<num, MicRes> _buildMicMap(List<MicRes> roomWheatList) {
final previousMap = roomWheatMap;
final nextMap = <num, MicRes>{};
final userSeatMap = <String, num>{};
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<MicRes> 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<num, MicRes> _stabilizeMicListSnapshotWithRecentMicChange(
Map<num, MicRes> nextMap,
) {
final nowMs = DateTime.now().millisecondsSinceEpoch;
_pruneRecentMicChangeSeatSnapshots(nowMs);
if (_recentMicChangeSeatSnapshots.isEmpty) {
return nextMap;
}
Map<num, MicRes>? stabilizedMap;
final confirmedSeatIndexes = <num>[];
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<num, MicRes>.from(nextMap);
stabilizedMap[seatIndex] = _micChangeAuthoritativeSeat(
incomingMic: incomingMic,
recentMic: recentMic,
seatIndex: seatIndex,
clearUser: true,
);
continue;
}
if (incomingUserId == recentUserId) {
confirmedSeatIndexes.add(seatIndex);
continue;
}
stabilizedMap ??= Map<num, MicRes>.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<MicRes> roomWheatList, {
bool notifyIfUnchanged = true,
}) {
final nextMap = _buildMicMap(roomWheatList);
final stabilizedNextMap = _stabilizeMicListSnapshotWithRecentMicChange(
nextMap,
);
_applyMicSnapshot(stabilizedNextMap, notifyIfUnchanged: notifyIfUnchanged);
}
@visibleForTesting
void debugApplyMicListSnapshot(
List<MicRes> 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 currentUserKeys = _currentUserIdentityKeys();
if (currentUserKeys.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 _userProfileMatchesAnyIdentity(resolvedSeat?.user, currentUserKeys);
}
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<num, MicRes> _stabilizeSelfMicSnapshot(
Map<num, MicRes> nextMap, {
required Map<num, MicRes> previousMap,
}) {
final currentUserKeys = _currentUserIdentityKeys();
if (currentUserKeys.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) => _userProfileMatchesAnyIdentity(
entry.value.user,
currentUserKeys,
),
)
.map((entry) => entry.key)
.toList();
if (targetIndex != null &&
_userProfileMatchesAnyIdentity(
nextMap[targetIndex]?.user,
currentUserKeys,
)) {
_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,
currentUserKeys: currentUserKeys,
);
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,
currentUserKeys: currentUserKeys,
);
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 (!_userProfileMatchesAnyIdentity(releaseSeat?.user, currentUserKeys)) {
_clearSelfMicReleaseGuard();
return nextMap;
}
final stabilizedMap = Map<num, MicRes>.from(nextMap);
stabilizedMap[releaseIndex] = releaseSeat!.copyWith(clearUser: true);
return stabilizedMap;
}
Map<num, MicRes>? _stabilizeOptimisticSelfMicSeat({
required Map<num, MicRes> nextMap,
required Map<num, MicRes> previousMap,
required Iterable<num> selfSeatIndices,
required num targetIndex,
required Set<String> currentUserKeys,
}) {
final optimisticTargetSeat = previousMap[targetIndex];
if (optimisticTargetSeat == null ||
!_userProfileMatchesAnyIdentity(
optimisticTargetSeat.user,
currentUserKeys,
)) {
return null;
}
final incomingTargetSeat = nextMap[targetIndex];
if (incomingTargetSeat?.user != null &&
!_userProfileMatchesAnyIdentity(
incomingTargetSeat?.user,
currentUserKeys,
)) {
return null;
}
final stabilizedMap = Map<num, MicRes>.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<num, MicRes> 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 currentUserKeys = _currentUserIdentityKeys();
if (currentUserKeys.isEmpty) {
return null;
}
for (final mic in roomWheatMap.values) {
if (_userProfileMatchesAnyIdentity(mic.user, currentUserKeys)) {
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<void> _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<num, MicRes> 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 = <String>{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<String> 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 = <String>{};
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 = <String>{};
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<void> _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<SocialChatUserProfile> _loadRoomMicRelationProfile(
SocialChatUserProfile profile,
String userId,
) async {
final cpPairs = <CPRes>[];
final closeFriendPairs = <CPRes>[];
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<List<CPRes>> _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 <CPRes>[];
}
}
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 <CPRes>[];
final nextCloseFriendList =
relationProfile.closeFriendList ?? const <CPRes>[];
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<CPRes>? left, List<CPRes>? right) {
final leftList = left ?? const <CPRes>[];
final rightList = right ?? const <CPRes>[];
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<num, MicRes> _withoutLocallyDismissedRoomMicRelationsFromMicMap(
Map<num, MicRes> 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 <CPRes>[];
return user.copyWith(
cpList: nextCpList,
closeFriendList: nextCloseFriendList,
isCpRelation: effectiveCpList.isNotEmpty,
);
}
List<CPRes>? _withoutLocallyDismissedRoomMicRelationList(
List<CPRes>? 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<String> _roomMicRelationDismissedPairKeysFromRelation(CPRes relation) {
final leftKeys = <String>{
relation.meUserId?.trim() ?? "",
relation.meAccount?.trim() ?? "",
}..remove("");
final rightKeys = <String>{
relation.cpUserId?.trim() ?? "",
relation.cpAccount?.trim() ?? "",
}..remove("");
if (leftKeys.isEmpty || rightKeys.isEmpty) {
return const <String>{};
}
final relationType = scNormalizeCpRelationType(relation.relationType);
return <String>{
for (final leftKey in leftKeys)
for (final rightKey in rightKeys)
_roomMicRelationDismissedPairKey(leftKey, rightKey, relationType),
};
}
String _roomMicRelationDismissedPairKey(
String leftIdentity,
String rightIdentity,
String relationType,
) {
final identities = <String>[leftIdentity.trim(), rightIdentity.trim()]
..sort();
return '${scNormalizeCpRelationType(relationType)}|'
'${identities[0]}|${identities[1]}';
}
Set<String> _roomMicRelationIdentityKeysForUserId(String userId) {
final normalizedUserId = userId.trim();
final keys = <String>{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<void> _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<void> _leaveRoomRtcForExit() async {
final adapter = _roomRtcEngineAdapter;
_resetRoomRtcTracking();
if (adapter == null) {
return;
}
try {
await adapter.leaveRoom();
} catch (error) {}
}
Future<void> _handleRoomRtcJoinFailed(
Object error, {
required bool exitRoom,
}) async {
await _cleanupRoomRtcState(clearMicSeats: true);
if (!exitRoom || _isExitingCurrentVoiceRoomSession) {
return;
}
SCTts.show("Join room fail");
unawaited(exitCurrentVoiceRoomSession(false));
}
Future<void> _handleRoomRtcDisconnected(Object reason) async {
if (_isHandlingRoomRtcFailure || _isExitingCurrentVoiceRoomSession) {
return;
}
_isHandlingRoomRtcFailure = true;
SCTts.show("Voice connection failed");
await _cleanupRoomRtcState(clearMicSeats: true);
await exitCurrentVoiceRoomSession(false);
}
Future<void> 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<String> _fetchRoomRtcJoinCredentialForCurrentRoom() async {
return _resolveTrtcUserSigForCurrentUser();
}
Future<_RoomStartupAsyncResult<T>> _captureStartupFuture<T>(
Future<T> future,
) async {
try {
return _RoomStartupAsyncResult<T>.value(await future);
} catch (error, stackTrace) {
return _RoomStartupAsyncResult<T>.error(error, stackTrace);
}
}
Future<void> 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<bool> _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<void> _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<bool> _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<void> 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<void> 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<void> releaseRtcEngineForAppTermination() async {
final adapter = _roomRtcEngineAdapter;
_roomRtcEngineAdapter = null;
_rtcEnginePrewarmTask = null;
_resetRoomRtcTracking();
try {
await adapter?.release();
} catch (e) {}
await SCVoiceRoomForegroundService.stop();
}
void _handleRoomRtcVoiceVolume(
List<RoomRtcUserVolume> userVolumes,
int totalVolume,
) {
if (roomWheatMap.isEmpty || userVolumes.isEmpty) {
return;
}
for (final seat in roomWheatMap.values) {
seat.setVolume = 0;
}
final seatIndexByUserId = <String, num>{};
roomWheatMap.forEach((seatIndex, mic) {
final userId = (mic.user?.id ?? "").trim();
if (userId.isEmpty) {
return;
}
seatIndexByUserId[userId] = seatIndex;
});
final listeners = List<OnSoundVoiceChange>.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<SocialChatUserProfileManager>(
context,
listen: false,
).fetchUserProfileData();
final groupId = currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "";
if (groupId.isNotEmpty) {
unawaited(
rtmProvider?.loadRoomHistoryMessages(groupId, count: 20) ??
Future<void>.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) {
_switchAwayFromCurrentRoomForNewEntry();
}
if (!context.mounted) {
return;
}
rtmProvider = Provider.of<RtmProvider>(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<void> _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<void> _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<SocialChatUserProfileManager>(
context,
listen: false,
).userIdentity;
if (userIdentity?.superAdmin == true ||
userIdentity?.admin == true ||
userIdentity?.yumiManager == true) {
return false;
}
return true;
}
Future<JoinRoomRes?> _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<void> _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<SCVipStatusRes?> _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 <String>[];
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<dynamic> 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<void> _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<void> 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<void> _runPersistedVoiceRoomCleanup(
_PersistedVoiceRoomCleanupRequest? cleanupRequest,
) {
if (cleanupRequest == null) {
return Future<void>.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<void> _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<void> 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<void> _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<void>.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<void> _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 = <String>{};
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<List<CPRes>> _safeFetchCurrentUserCpRelations(
String relationType,
) async {
try {
return await SCAccountRepository().cpRelationshipPairs(
relationType: relationType,
);
} catch (error) {
debugPrint(
"[CP][RoomEntry] pair failed relationType=$relationType error=$error",
);
return const <CPRes>[];
}
}
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<String> _relationProfileKeys(SocialChatUserProfile profile) {
return {
profile.id?.trim() ?? "",
profile.account?.trim() ?? "",
profile.getID().trim(),
}..removeWhere((value) => value.isEmpty);
}
Set<String> _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<String?> 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<void> 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 <SCRoomRocketLevelRes>[]),
]..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 <SCRoomRocketLevelRes>[]) {
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<String?>(
_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 <SCRoomRocketKingUserRes>[];
return _roomRocketTop1AvatarUrlFromUsers(kings);
}
String? _roomRocketTop1AvatarUrlFromKing(SCRoomRocketKingRes king) {
final source = king.podium.isNotEmpty ? king.podium : king.displayRecords;
return _roomRocketTop1AvatarUrlFromUsers(source);
}
String? _roomRocketTop1AvatarUrlFromUsers(
List<SCRoomRocketKingUserRes> 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<void> _loadRoomRocketTop1Avatar(
RoomRocketLaunchBroadcastMessage launch,
ValueNotifier<String?> 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<String?>? 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 =
<String>{
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<SocialChatUserProfile> _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<String> keys,
) {
return [profile.id, profile.account, profile.inRoomId].any((value) {
final text = value?.trim() ?? '';
return text.isNotEmpty && keys.contains(text);
});
}
///获取在线用户
Future<void> 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<void> 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<void>.microtask(
() => retrieveMicrophoneList(
notifyIfUnchanged: notifyIfUnchanged || pendingNotifyIfUnchanged,
),
),
);
}
}
}
void fetchRoomTaskClaimableCount() {
SCChatRoomRepository()
.roomTaskClaimableCount()
.then((res) {
roomTaskClaimableCount = res.claimableCount ?? 0;
notifyListeners();
})
.catchError((e) {});
}
Future<List<SCRoomRedPacketListRes>> loadRoomRedPacketList(
int current,
) async {
final roomId = currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? "";
if (roomId.isEmpty) {
return const <SCRoomRedPacketListRes>[];
}
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<void>.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<void>.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<void>.delayed(delay).then((_) {
return _refreshRoomRedPacketListIfCurrent(normalizedRoomId);
}),
);
}
}
Future<void> _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) {}
_clearData(clearPersistedRoomMarker: false);
if (!isLogout && navigationContext != null) {
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;
}
}
void _switchAwayFromCurrentRoomForNewEntry() {
_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) {}
_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<void> _cleanupExitedRoomRemotely({
required RtmProvider? exitRtmProvider,
required String groupId,
required String roomId,
required bool shouldSendOnlineHeartbeat,
}) async {
final tasks = <Future<void>>[];
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<void> _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<void> _retryExitNetworkRequest(
String name,
Future<void> 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<void> 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<void> 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) {
///是自己,直接打开资料卡
showBottomInBottomDialog(
context!,
RoomUserInfoCard(userId: clickUser.id),
);
} else {
showBottomInBottomDialog(
context!,
EmptyMaiSelect(index: index, clickUser: clickUser),
);
}
}
} else {
SocialChatUserProfile? roomWheatUser = micAtIndexForDisplay(index)?.user;
showBottomInBottomDialog(
context!,
EmptyMaiSelect(index: index, clickUser: roomWheatUser),
);
// if (roomWheatUser != null) {
// ///麦上有人
// if (roomWheatUser.id ==
// AccountStorage().getCurrentUser()?.userProfile?.id) {
// ///是自己
// showBottomInBottomDialog(
// context!,
// EmptyMaiSelect(index: index, clickUser: roomWheatUser),
// );
// } else {
// showBottomInBottomDialog(
// context!,
// RoomUserInfoCard(userId: roomWheatUser.id),
// );
// }
// } else {
// showBottomInBottomDialog(
// context!,
// EmptyMaiSelect(index: index, clickUser: roomWheatUser),
// );
// }
}
}
bool _handleDirectSeatInteraction(
num index, {
SocialChatUserProfile? clickUser,
}) {
final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id;
final isRoomAdmin = isFz() || isGL();
if (index == -1) {
if (clickUser == null || (clickUser.id ?? '').isEmpty) {
return false;
}
if (clickUser.id == currentUserId || !isRoomAdmin) {
_openRoomUserInfoCard(clickUser.id);
return true;
}
return false;
}
final seat = micAtIndexForDisplay(index);
final seatUser = seat?.user;
if (seatUser == null) {
if ((seat?.micLock ?? false) && !isRoomAdmin) {
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;
}
showBottomInBottomDialog(
context!,
RoomUserInfoCard(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<void> 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<RtmProvider>(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) {
final normalizedUserId = userId.trim();
if (normalizedUserId.isEmpty) {
return -1;
}
num index = -1;
final currentUserKeys = _currentUserIdentityKeys();
final isCurrentUserQuery = currentUserKeys.contains(normalizedUserId);
final lookupKeys =
isCurrentUserQuery ? currentUserKeys : <String>{normalizedUserId}
..removeWhere((item) => item.isEmpty);
roomWheatMap.forEach((k, value) {
final visibleSeat = isCurrentUserQuery ? micAtIndexForDisplay(k) : value;
if (_userProfileMatchesAnyIdentity(visibleSeat?.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<MicRes>? 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<bool> 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<void> 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<void> 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<void> 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<void> 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<RtcProvider>(
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<RtcProvider>(
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<num, GlobalKey<State<StatefulWidget>>> seatGlobalKeyMap = {};
void bindTargetKey(num index, GlobalKey<State<StatefulWidget>> targetKey) {
seatGlobalKeyMap[index] = targetKey;
}
GlobalKey<State<StatefulWidget>>? 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 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<RtmProvider>(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<void>.value(),
);
}
void republishCurrentUserRoomMusicStateIfNeeded() {
final currentUserId =
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
if (isUserPublishingRoomMusic(currentUserId)) {
publishCurrentUserRoomMusicState(true);
}
}
Future<void> 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<void> pauseRoomMusicAudioMixing() async {
await _roomRtcEngineAdapter?.pauseAudioMixing();
}
Future<void> resumeRoomMusicAudioMixing() async {
await _roomRtcEngineAdapter?.resumeAudioMixing();
}
Future<void> stopRoomMusicAudioMixing() async {
await _roomRtcEngineAdapter?.stopAudioMixing();
}
Future<void> setRoomMusicAudioMixingPosition(int positionMs) async {
await _roomRtcEngineAdapter?.setAudioMixingPosition(positionMs);
}
Future<int> getRoomMusicAudioMixingPosition() async {
return await _roomRtcEngineAdapter?.getAudioMixingPosition() ?? 0;
}
Future<int> getRoomMusicAudioMixingDuration() async {
return await _roomRtcEngineAdapter?.getAudioMixingDuration() ?? 0;
}
Future<void> setRoomMusicAudioMixingVolume(int volume) async {
final safeVolume = volume.clamp(0, 100).toInt();
await _roomRtcEngineAdapter?.setAudioMixingVolume(safeVolume);
}
Future<bool> _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,
);
}
}