5998 lines
184 KiB
Dart
5998 lines
184 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 RoomMusicSessionExitListener = Future<void> Function();
|
|
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;
|
|
RoomMusicSessionExitListener? _roomMusicSessionExitListener;
|
|
BuildContext? context;
|
|
RtmProvider? rtmProvider;
|
|
|
|
SCIsFollowRoomRes? isFollowRoomRes;
|
|
SCRoomRocketStatusRes? roomRocketStatus;
|
|
|
|
///房间是否静音
|
|
bool roomIsMute = false;
|
|
|
|
bool closeFullGame = false;
|
|
|
|
///禁音开关 默认关闭
|
|
bool isMic = true;
|
|
|
|
bool shouldShowSeatSoundWave(num index) {
|
|
final seat = roomWheatMap[index];
|
|
final userId = (seat?.user?.id ?? "").trim();
|
|
if (userId.isEmpty || (seat?.micMute ?? false)) {
|
|
return false;
|
|
}
|
|
|
|
final currentUserId =
|
|
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
|
|
if (userId == currentUserId && isMic) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
///在线用户列表
|
|
List<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() {
|
|
return _userProfileIdentityKeys(
|
|
AccountStorage().getCurrentUser()?.userProfile,
|
|
);
|
|
}
|
|
|
|
Set<String> _userProfileIdentityKeys(SocialChatUserProfile? user) {
|
|
return {
|
|
user?.id?.trim() ?? "",
|
|
user?.account?.trim() ?? "",
|
|
user?.getID().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 currentUserId =
|
|
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
|
|
if (currentUserId.isEmpty) {
|
|
return false;
|
|
}
|
|
final releaseIndex = _pendingSelfMicReleaseIndex;
|
|
final releaseGuardUntilMs = _pendingSelfMicReleaseGuardUntilMs ?? 0;
|
|
if (releaseIndex != index ||
|
|
releaseGuardUntilMs <= DateTime.now().millisecondsSinceEpoch) {
|
|
return false;
|
|
}
|
|
final resolvedSeat = seat ?? roomWheatMap[index];
|
|
return (resolvedSeat?.user?.id ?? "").trim() == currentUserId;
|
|
}
|
|
|
|
MicRes? micAtIndexForDisplay(num index) {
|
|
final seat = roomWheatMap[index];
|
|
if (!_shouldSuppressCurrentUserOnSeat(index, seat: seat)) {
|
|
return seat;
|
|
}
|
|
return seat?.copyWith(clearUser: true);
|
|
}
|
|
|
|
int emojiEventVersionForSeat(num index) {
|
|
return _seatEmojiEventVersions[index] ?? 0;
|
|
}
|
|
|
|
void consumeSeatEmojiEvent(num index, int eventVersion) {
|
|
if (emojiEventVersionForSeat(index) != eventVersion) {
|
|
return;
|
|
}
|
|
_seatEmojiEventVersions.remove(index);
|
|
roomWheatMap[index]?.setEmojiPath = null;
|
|
}
|
|
|
|
Map<num, MicRes> _stabilizeSelfMicSnapshot(
|
|
Map<num, MicRes> nextMap, {
|
|
required Map<num, MicRes> previousMap,
|
|
}) {
|
|
final currentUserId =
|
|
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
|
|
if (currentUserId.isEmpty) {
|
|
return nextMap;
|
|
}
|
|
final nowMs = DateTime.now().millisecondsSinceEpoch;
|
|
|
|
final targetIndex = _preferredSelfMicIndex;
|
|
final sourceIndex = _pendingSelfMicSourceIndex;
|
|
final guardUntilMs = _pendingSelfMicSwitchGuardUntilMs ?? 0;
|
|
final hasActiveGuard =
|
|
targetIndex != null && sourceIndex != null && guardUntilMs > nowMs;
|
|
final selfSeatIndices =
|
|
nextMap.entries
|
|
.where((entry) => entry.value.user?.id == currentUserId)
|
|
.map((entry) => entry.key)
|
|
.toList();
|
|
|
|
if (targetIndex != null &&
|
|
nextMap[targetIndex]?.user?.id == currentUserId) {
|
|
_clearSelfMicSwitchGuard();
|
|
_clearSelfMicGoUpGuard();
|
|
return nextMap;
|
|
}
|
|
|
|
final goUpIndex = _pendingSelfMicGoUpIndex;
|
|
final goUpGuardUntilMs = _pendingSelfMicGoUpGuardUntilMs ?? 0;
|
|
final hasActiveGoUpGuard = goUpIndex != null && goUpGuardUntilMs > nowMs;
|
|
if (!hasActiveGoUpGuard) {
|
|
_clearSelfMicGoUpGuard();
|
|
} else if (selfSeatIndices.isEmpty ||
|
|
selfSeatIndices.every((seatIndex) => seatIndex == goUpIndex)) {
|
|
final stabilizedMap = _stabilizeOptimisticSelfMicSeat(
|
|
nextMap: nextMap,
|
|
previousMap: previousMap,
|
|
selfSeatIndices: selfSeatIndices,
|
|
targetIndex: goUpIndex,
|
|
currentUserId: currentUserId,
|
|
);
|
|
if (stabilizedMap != null) {
|
|
return stabilizedMap;
|
|
}
|
|
} else {
|
|
_clearSelfMicGoUpGuard();
|
|
}
|
|
|
|
if (!hasActiveGuard) {
|
|
_clearSelfMicSwitchGuard();
|
|
} else {
|
|
final resolvedTargetIndex = targetIndex;
|
|
final resolvedSourceIndex = sourceIndex;
|
|
|
|
final selfOnlyOnSource =
|
|
selfSeatIndices.isEmpty ||
|
|
selfSeatIndices.every(
|
|
(seatIndex) => seatIndex == resolvedSourceIndex,
|
|
);
|
|
if (selfOnlyOnSource) {
|
|
final stabilizedMap = _stabilizeOptimisticSelfMicSeat(
|
|
nextMap: nextMap,
|
|
previousMap: previousMap,
|
|
selfSeatIndices: selfSeatIndices,
|
|
targetIndex: resolvedTargetIndex,
|
|
currentUserId: currentUserId,
|
|
);
|
|
if (stabilizedMap != null) {
|
|
return stabilizedMap;
|
|
}
|
|
}
|
|
}
|
|
|
|
final releaseIndex = _pendingSelfMicReleaseIndex;
|
|
final releaseGuardUntilMs = _pendingSelfMicReleaseGuardUntilMs ?? 0;
|
|
final hasActiveReleaseGuard =
|
|
releaseIndex != null && releaseGuardUntilMs > nowMs;
|
|
if (!hasActiveReleaseGuard) {
|
|
_clearSelfMicReleaseGuard();
|
|
return nextMap;
|
|
}
|
|
|
|
if (selfSeatIndices.isEmpty) {
|
|
return nextMap;
|
|
}
|
|
|
|
if (selfSeatIndices.any((seatIndex) => seatIndex != releaseIndex)) {
|
|
_clearSelfMicReleaseGuard();
|
|
return nextMap;
|
|
}
|
|
|
|
final releaseSeat = nextMap[releaseIndex];
|
|
if (releaseSeat?.user?.id != currentUserId) {
|
|
_clearSelfMicReleaseGuard();
|
|
return nextMap;
|
|
}
|
|
|
|
final stabilizedMap = Map<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 String currentUserId,
|
|
}) {
|
|
final optimisticTargetSeat = previousMap[targetIndex];
|
|
if (optimisticTargetSeat == null ||
|
|
optimisticTargetSeat.user?.id != currentUserId) {
|
|
return null;
|
|
}
|
|
final incomingTargetSeat = nextMap[targetIndex];
|
|
if (incomingTargetSeat?.user != null &&
|
|
incomingTargetSeat?.user?.id != currentUserId) {
|
|
return null;
|
|
}
|
|
|
|
final stabilizedMap = Map<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 currentUserId =
|
|
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
|
|
if (currentUserId.isEmpty) {
|
|
return null;
|
|
}
|
|
for (final mic in roomWheatMap.values) {
|
|
if ((mic.user?.id ?? "").trim() == currentUserId) {
|
|
return mic;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
void _syncSelfMicRuntimeState() {
|
|
final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id;
|
|
if ((currentUserId ?? "").isEmpty) {
|
|
return;
|
|
}
|
|
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
|
|
|
final currentUserMic = _currentUserMicSeat();
|
|
|
|
if (currentUserMic == null) {
|
|
_clearSelfMicSwitchGuard(clearPreferredIndex: true);
|
|
_clearSelfMicGoUpGuard(clearPreferredIndex: true);
|
|
_roomRtcRoleIsBroadcaster = false;
|
|
_syncRoomHeartbeatState(
|
|
roomId: roomId,
|
|
isOnMic: false,
|
|
shouldRunAnchorHeartbeat: false,
|
|
);
|
|
_applyLocalAudioRuntimeState(
|
|
role: _RoomRtcLocalAudioRole.audience,
|
|
muted: true,
|
|
);
|
|
_roomMusicMicRouteChangedListener?.call(false);
|
|
return;
|
|
}
|
|
|
|
_preferredSelfMicIndex = currentUserMic.micIndex;
|
|
final canReportMicActive = _canReportMicActive;
|
|
_syncRoomHeartbeatState(
|
|
roomId: roomId,
|
|
isOnMic: canReportMicActive,
|
|
shouldRunAnchorHeartbeat: canReportMicActive,
|
|
);
|
|
|
|
_applyLocalAudioRuntimeState(
|
|
role: _RoomRtcLocalAudioRole.anchor,
|
|
muted: (currentUserMic.micMute ?? false) || isMic,
|
|
);
|
|
_roomMusicMicRouteChangedListener?.call(true);
|
|
}
|
|
|
|
void _syncRoomHeartbeatState({
|
|
required String roomId,
|
|
required bool isOnMic,
|
|
required bool shouldRunAnchorHeartbeat,
|
|
}) {
|
|
final normalizedRoomId = roomId.trim();
|
|
if (normalizedRoomId.isEmpty) {
|
|
_resetHeartbeatTracking();
|
|
return;
|
|
}
|
|
|
|
if (!_roomRtcJoined) {
|
|
if (_lastScheduledVoiceLiveRoomId != null ||
|
|
_lastScheduledAnchorRoomId != null) {
|
|
SCHeartbeatUtils.scheduleHeartbeat(
|
|
SCHeartbeatStatus.ONLINE.name,
|
|
false,
|
|
);
|
|
SCHeartbeatUtils.cancelAnchorTimer();
|
|
}
|
|
_resetHeartbeatTracking();
|
|
return;
|
|
}
|
|
|
|
final voiceLiveChanged =
|
|
_lastScheduledVoiceLiveRoomId != normalizedRoomId ||
|
|
_lastScheduledVoiceLiveOnMic != isOnMic;
|
|
if (voiceLiveChanged) {
|
|
SCHeartbeatUtils.scheduleHeartbeat(
|
|
SCHeartbeatStatus.VOICE_LIVE.name,
|
|
isOnMic,
|
|
roomId: normalizedRoomId,
|
|
);
|
|
_lastScheduledVoiceLiveRoomId = normalizedRoomId;
|
|
_lastScheduledVoiceLiveOnMic = isOnMic;
|
|
}
|
|
|
|
if (!shouldRunAnchorHeartbeat) {
|
|
if (_lastScheduledAnchorRoomId != null) {
|
|
SCHeartbeatUtils.cancelAnchorTimer();
|
|
_lastScheduledAnchorRoomId = null;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (voiceLiveChanged || _lastScheduledAnchorRoomId != normalizedRoomId) {
|
|
SCHeartbeatUtils.scheduleAnchorHeartbeat(normalizedRoomId);
|
|
_lastScheduledAnchorRoomId = normalizedRoomId;
|
|
}
|
|
}
|
|
|
|
Future<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) {
|
|
await _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) {}
|
|
|
|
await _notifyRoomMusicSessionExit();
|
|
_clearData(clearPersistedRoomMarker: false);
|
|
if (!isLogout && navigationContext != null && navigationContext.mounted) {
|
|
VoiceRoomRoute.popVoiceRoomToPrevious(navigationContext);
|
|
}
|
|
|
|
final roomRtcLeaveTask = _leaveRoomRtcForExit();
|
|
final remoteCleanupTask = _cleanupExitedRoomRemotely(
|
|
exitRtmProvider: exitRtmProvider,
|
|
groupId: isPreviewOnlyExit ? "" : groupId,
|
|
roomId: isPreviewOnlyExit ? "" : roomId,
|
|
shouldSendOnlineHeartbeat: !isLogout,
|
|
);
|
|
if (deferRemoteCleanup) {
|
|
unawaited(roomRtcLeaveTask);
|
|
unawaited(remoteCleanupTask);
|
|
} else {
|
|
await roomRtcLeaveTask;
|
|
await remoteCleanupTask;
|
|
}
|
|
}
|
|
|
|
Future<void> _switchAwayFromCurrentRoomForNewEntry() async {
|
|
_stopRoomStatePolling();
|
|
final groupId = currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "";
|
|
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
|
final isPreviewOnlyExit = _currentRoomIsEntryPreview;
|
|
final exitRtmProvider = rtmProvider;
|
|
try {
|
|
SCGiftVapSvgaManager().stopPlayback();
|
|
setRoomVisualEffectsEnabled(false);
|
|
SCHeartbeatUtils.cancelTimer();
|
|
_stopRoomRedPacketPresenceHeartbeat();
|
|
exitRtmProvider?.cleanRoomData();
|
|
} catch (e) {}
|
|
|
|
await _notifyRoomMusicSessionExit();
|
|
_clearData(clearPersistedRoomMarker: false);
|
|
final roomRtcLeaveTask = _leaveRoomRtcForExit();
|
|
_pendingRoomSwitchRtcLeaveTask = roomRtcLeaveTask;
|
|
unawaited(
|
|
roomRtcLeaveTask.whenComplete(() {
|
|
if (identical(_pendingRoomSwitchRtcLeaveTask, roomRtcLeaveTask)) {
|
|
_pendingRoomSwitchRtcLeaveTask = null;
|
|
}
|
|
}),
|
|
);
|
|
unawaited(
|
|
_cleanupExitedRoomRemotely(
|
|
exitRtmProvider: exitRtmProvider,
|
|
groupId: isPreviewOnlyExit ? "" : groupId,
|
|
roomId: isPreviewOnlyExit ? "" : roomId,
|
|
shouldSendOnlineHeartbeat: false,
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<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) {
|
|
///是自己,直接打开资料卡
|
|
RoomUserInfoCard.showPreloaded(context!, userId: clickUser.id);
|
|
} else {
|
|
showBottomInBottomDialog(
|
|
context!,
|
|
EmptyMaiSelect(index: index, clickUser: clickUser),
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
SocialChatUserProfile? roomWheatUser = micAtIndexForDisplay(index)?.user;
|
|
showBottomInBottomDialog(
|
|
context!,
|
|
EmptyMaiSelect(index: index, clickUser: roomWheatUser),
|
|
);
|
|
// if (roomWheatUser != null) {
|
|
// ///麦上有人
|
|
// if (roomWheatUser.id ==
|
|
// AccountStorage().getCurrentUser()?.userProfile?.id) {
|
|
// ///是自己
|
|
// showBottomInBottomDialog(
|
|
// context!,
|
|
// EmptyMaiSelect(index: index, clickUser: roomWheatUser),
|
|
// );
|
|
// } else {
|
|
// showBottomInBottomDialog(
|
|
// context!,
|
|
// RoomUserInfoCard(userId: roomWheatUser.id),
|
|
// );
|
|
// }
|
|
// } else {
|
|
// showBottomInBottomDialog(
|
|
// context!,
|
|
// EmptyMaiSelect(index: index, clickUser: roomWheatUser),
|
|
// );
|
|
// }
|
|
}
|
|
}
|
|
|
|
bool _handleDirectSeatInteraction(
|
|
num index, {
|
|
SocialChatUserProfile? clickUser,
|
|
}) {
|
|
final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id;
|
|
final isRoomAdmin = isFz() || isGL();
|
|
|
|
if (index == -1) {
|
|
if (clickUser == null || (clickUser.id ?? '').isEmpty) {
|
|
return false;
|
|
}
|
|
|
|
if (clickUser.id == currentUserId || !isRoomAdmin) {
|
|
_openRoomUserInfoCard(clickUser.id);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
final seat = micAtIndexForDisplay(index);
|
|
final seatUser = seat?.user;
|
|
if (seatUser == null) {
|
|
if ((seat?.micLock ?? false) && !isRoomAdmin) {
|
|
return true;
|
|
}
|
|
if (!(seat?.micLock ?? false) && !isRoomAdmin) {
|
|
shangMai(index);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (seatUser.id == currentUserId) {
|
|
if (isRoomAdmin) {
|
|
return false;
|
|
}
|
|
_openRoomUserInfoCard(seatUser.id);
|
|
return true;
|
|
}
|
|
|
|
if (!isRoomAdmin && (seatUser.id ?? '').isNotEmpty) {
|
|
_openRoomUserInfoCard(seatUser.id);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void _refreshMicListSilently({bool notifyIfUnchanged = false}) {
|
|
retrieveMicrophoneList(
|
|
notifyIfUnchanged: notifyIfUnchanged,
|
|
).catchError((_) {});
|
|
}
|
|
|
|
void _clearUserFromSeats(String? userId, {num? exceptIndex}) {
|
|
final normalizedUserId = (userId ?? "").trim();
|
|
if (normalizedUserId.isEmpty) {
|
|
return;
|
|
}
|
|
roomWheatMap.forEach((seatIndex, seat) {
|
|
if (seat.user?.id != normalizedUserId || seatIndex == exceptIndex) {
|
|
return;
|
|
}
|
|
roomWheatMap[seatIndex] = seat.copyWith(clearUser: true);
|
|
});
|
|
}
|
|
|
|
void _openRoomUserInfoCard(String? userId) {
|
|
final normalizedUserId = (userId ?? '').trim();
|
|
if (normalizedUserId.isEmpty) {
|
|
return;
|
|
}
|
|
RoomUserInfoCard.showPreloaded(context!, userId: normalizedUserId);
|
|
}
|
|
|
|
addSoundVoiceChangeListener(OnSoundVoiceChange onSoundVoiceChange) {
|
|
_onSoundVoiceChangeList.add(onSoundVoiceChange);
|
|
}
|
|
|
|
removeSoundVoiceChangeListener(OnSoundVoiceChange onSoundVoiceChange) {
|
|
_onSoundVoiceChangeList.remove(onSoundVoiceChange);
|
|
}
|
|
|
|
void shangMai(num index, {String? eventType, String? inviterId}) async {
|
|
if (_shouldDeferSeatInteractionForRoomStartup) {
|
|
_deferRoomStartupSeatAction(
|
|
_PendingRoomStartupSeatAction.goUpMic(
|
|
index: index,
|
|
eventType: eventType,
|
|
inviterId: inviterId,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
var myUser = AccountStorage().getCurrentUser()?.userProfile;
|
|
if (currenRoom?.roomProfile?.roomSetting?.touristMike ?? false) {
|
|
} else {
|
|
if (isTourists()) {
|
|
SCTts.show(
|
|
SCAppLocalizations.of(context!)!.touristsAreNotAllowedToGoOnTheMic,
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
final shouldShowRetryLoading = _selfMicGoUpConsecutiveFailureCount > 0;
|
|
if (_isSelfMicActionInFlight) {
|
|
if (shouldShowRetryLoading) {
|
|
_showSelfMicGoUpRetryLoading();
|
|
}
|
|
return;
|
|
}
|
|
if (shouldShowRetryLoading) {
|
|
_showSelfMicGoUpRetryLoading();
|
|
}
|
|
_isSelfMicActionInFlight = true;
|
|
try {
|
|
var micGoUpRes = await SCChatRoomRepository().micGoUp(
|
|
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
|
index,
|
|
eventType: eventType,
|
|
inviterId: inviterId,
|
|
);
|
|
|
|
final targetIndex = micGoUpRes.micIndex ?? index;
|
|
final selfMicMuted = micGoUpRes.micMute ?? false;
|
|
final switched = await _switchRoomRtcToBroadcaster(
|
|
muted: selfMicMuted,
|
|
keepAudioPublishing: selfMicMuted && _currentUserPublishingRoomMusic,
|
|
);
|
|
if (!switched) {
|
|
await _switchRoomRtcToAudience();
|
|
try {
|
|
await SCChatRoomRepository().micGoDown(
|
|
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
|
targetIndex,
|
|
);
|
|
} catch (downError) {}
|
|
_clearSelfMicSwitchGuard(clearPreferredIndex: true);
|
|
_clearSelfMicGoUpGuard(clearPreferredIndex: true);
|
|
_clearUserFromSeats(myUser?.id);
|
|
_syncSelfMicRuntimeState();
|
|
notifyListeners();
|
|
_refreshMicListSilently(notifyIfUnchanged: true);
|
|
_handleSelfMicGoUpFailure(
|
|
'Failed to put on the microphone, TRTC switch failed',
|
|
);
|
|
return;
|
|
}
|
|
|
|
final previousSelfSeatIndex =
|
|
myUser != null ? userOnMaiInIndex(myUser.id ?? "") : -1;
|
|
final isSeatSwitching =
|
|
previousSelfSeatIndex > -1 && previousSelfSeatIndex != targetIndex;
|
|
if (myUser != null) {
|
|
_clearSelfMicReleaseGuard();
|
|
if (isSeatSwitching) {
|
|
_startSelfMicSwitchGuard(
|
|
sourceIndex: previousSelfSeatIndex,
|
|
targetIndex: targetIndex,
|
|
);
|
|
} else {
|
|
_clearSelfMicSwitchGuard();
|
|
_startSelfMicGoUpGuard(targetIndex: targetIndex);
|
|
}
|
|
_clearUserFromSeats(myUser.id, exceptIndex: targetIndex);
|
|
final currentSeat = roomWheatMap[targetIndex];
|
|
final currentUser = myUser.copyWith(roles: currenRoom?.entrants?.roles);
|
|
roomWheatMap[targetIndex] =
|
|
currentSeat?.copyWith(
|
|
user: currentUser,
|
|
micMute: micGoUpRes.micMute,
|
|
micLock: micGoUpRes.micLock,
|
|
roomToken: micGoUpRes.roomToken,
|
|
) ??
|
|
MicRes(
|
|
roomId: currenRoom?.roomProfile?.roomProfile?.id,
|
|
micIndex: targetIndex,
|
|
micLock: micGoUpRes.micLock,
|
|
micMute: micGoUpRes.micMute,
|
|
user: currentUser,
|
|
roomToken: micGoUpRes.roomToken,
|
|
);
|
|
}
|
|
isMic = selfMicMuted;
|
|
_syncSelfMicRuntimeState();
|
|
if (roomWheatMap[targetIndex]?.micMute ?? false) {
|
|
///房主上麦自动解禁麦位
|
|
if (isFz()) {
|
|
await jieJinMai(targetIndex);
|
|
}
|
|
}
|
|
|
|
notifyListeners();
|
|
_refreshMicListSilently(notifyIfUnchanged: true);
|
|
_resetSelfMicGoUpFailureState();
|
|
} catch (ex) {
|
|
if (_isCancelledRequest(ex)) {
|
|
_refreshMicListSilently(notifyIfUnchanged: true);
|
|
return;
|
|
}
|
|
_handleSelfMicGoUpFailure(
|
|
'Failed to put on the microphone, ${_errorMessageFrom(ex)}',
|
|
);
|
|
} finally {
|
|
_dismissSelfMicGoUpRetryLoading();
|
|
_isSelfMicActionInFlight = false;
|
|
}
|
|
}
|
|
|
|
xiaMai(num index) async {
|
|
if (_isSelfMicActionInFlight) {
|
|
return;
|
|
}
|
|
_isSelfMicActionInFlight = true;
|
|
try {
|
|
await SCChatRoomRepository().micGoDown(
|
|
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
|
index,
|
|
);
|
|
|
|
final currentUserId =
|
|
AccountStorage().getCurrentUser()?.userProfile?.id ?? "";
|
|
final currentUserSeatIndex = userOnMaiInIndex(currentUserId);
|
|
if (roomWheatMap[index]?.user?.id == currentUserId) {
|
|
isMic = true;
|
|
}
|
|
_clearSelfMicSwitchGuard(clearPreferredIndex: true);
|
|
_clearSelfMicGoUpGuard(clearPreferredIndex: true);
|
|
if (currentUserSeatIndex > -1) {
|
|
_startSelfMicReleaseGuard(sourceIndex: currentUserSeatIndex);
|
|
} else {
|
|
_clearSelfMicReleaseGuard();
|
|
}
|
|
SCHeartbeatUtils.cancelAnchorTimer();
|
|
|
|
await _switchRoomRtcToAudience();
|
|
_clearUserFromSeats(currentUserId);
|
|
_syncSelfMicRuntimeState();
|
|
notifyListeners();
|
|
requestMicrophoneListRefresh(
|
|
notifyIfUnchanged: false,
|
|
minInterval: const Duration(milliseconds: 350),
|
|
);
|
|
} catch (ex) {
|
|
if (_isCancelledRequest(ex)) {
|
|
_refreshMicListSilently(notifyIfUnchanged: true);
|
|
return;
|
|
}
|
|
SCTts.show('Failed to leave the microphone, ${_errorMessageFrom(ex)}');
|
|
} finally {
|
|
_isSelfMicActionInFlight = false;
|
|
}
|
|
}
|
|
|
|
///踢人下麦
|
|
Future<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) {
|
|
return _findUserOnMaiInIndex(userId, useDisplaySeatForCurrentUser: true);
|
|
}
|
|
|
|
num _findUserOnMaiInIndex(
|
|
String userId, {
|
|
required bool useDisplaySeatForCurrentUser,
|
|
}) {
|
|
final normalizedUserId = userId.trim();
|
|
if (normalizedUserId.isEmpty) {
|
|
return -1;
|
|
}
|
|
num index = -1;
|
|
final currentUserKeys = _currentUserIdentityKeys();
|
|
final isCurrentUserQuery = currentUserKeys.contains(normalizedUserId);
|
|
final lookupKeys = <String>{normalizedUserId};
|
|
if (isCurrentUserQuery) {
|
|
lookupKeys.addAll(currentUserKeys);
|
|
}
|
|
lookupKeys.removeWhere((item) => item.isEmpty);
|
|
roomWheatMap.forEach((k, value) {
|
|
final candidateSeat =
|
|
useDisplaySeatForCurrentUser && isCurrentUserQuery
|
|
? micAtIndexForDisplay(k)
|
|
: value;
|
|
if (_userProfileMatchesAnyIdentity(candidateSeat?.user, lookupKeys)) {
|
|
index = k;
|
|
}
|
|
});
|
|
return index;
|
|
}
|
|
|
|
///座位上的用户是否开麦
|
|
bool userOnMaiInIndexOfMute(String userId) {
|
|
bool micMute = false;
|
|
roomWheatMap.forEach((k, value) {
|
|
if (value.user?.id == userId) {
|
|
value.micMute;
|
|
}
|
|
});
|
|
return micMute;
|
|
}
|
|
|
|
///是否是房主
|
|
bool isFz() {
|
|
return currenRoom?.entrants?.roles == SCRoomRolesType.HOMEOWNER.name;
|
|
}
|
|
|
|
///是否是管理
|
|
bool isGL() {
|
|
return currenRoom?.entrants?.roles == SCRoomRolesType.ADMIN.name;
|
|
}
|
|
|
|
///是否是会员
|
|
bool isHY() {
|
|
return currenRoom?.entrants?.roles == SCRoomRolesType.MEMBER.name;
|
|
}
|
|
|
|
///是否是游客
|
|
bool isTourists() {
|
|
return currenRoom?.entrants?.roles == SCRoomRolesType.TOURIST.name;
|
|
}
|
|
|
|
///麦位变动
|
|
void micChange(List<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 setRoomMusicSessionExitListener(RoomMusicSessionExitListener? listener) {
|
|
_roomMusicSessionExitListener = listener;
|
|
}
|
|
|
|
Future<void> _notifyRoomMusicSessionExit() async {
|
|
final listener = _roomMusicSessionExitListener;
|
|
if (listener == null) {
|
|
return;
|
|
}
|
|
try {
|
|
await listener();
|
|
} catch (_) {}
|
|
}
|
|
|
|
void updateRoomMusicPublishingState({
|
|
required String? userId,
|
|
required bool publishing,
|
|
bool notifyIfChanged = true,
|
|
}) {
|
|
final normalizedUserId = (userId ?? "").trim();
|
|
if (normalizedUserId.isEmpty) {
|
|
return;
|
|
}
|
|
bool changed = false;
|
|
if (publishing) {
|
|
changed = _roomMusicPublishingUserIds.add(normalizedUserId);
|
|
_roomMusicPublishingClearTimers.remove(normalizedUserId)?.cancel();
|
|
_roomMusicPublishingClearTimers[normalizedUserId] = Timer(
|
|
_roomMusicPublishingStateTtl,
|
|
() {
|
|
_roomMusicPublishingClearTimers.remove(normalizedUserId);
|
|
if (_roomMusicPublishingUserIds.remove(normalizedUserId)) {
|
|
if (_isCurrentUserId(normalizedUserId)) {
|
|
_syncSelfMicRuntimeState();
|
|
}
|
|
notifyListeners();
|
|
}
|
|
},
|
|
);
|
|
} else {
|
|
_roomMusicPublishingClearTimers.remove(normalizedUserId)?.cancel();
|
|
changed = _roomMusicPublishingUserIds.remove(normalizedUserId);
|
|
}
|
|
if (changed && _isCurrentUserId(normalizedUserId)) {
|
|
_syncSelfMicRuntimeState();
|
|
}
|
|
if (changed && notifyIfChanged) {
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
void _clearRoomMusicPublishingStates() {
|
|
for (final timer in _roomMusicPublishingClearTimers.values) {
|
|
timer.cancel();
|
|
}
|
|
_roomMusicPublishingClearTimers.clear();
|
|
_roomMusicPublishingUserIds.clear();
|
|
}
|
|
|
|
void handleRoomMusicPublishingMessage(Msg msg) {
|
|
final userId =
|
|
(msg.role ?? "").trim().isNotEmpty ? msg.role : msg.user?.id ?? "";
|
|
final statusText = (msg.msg ?? "").trim().toLowerCase();
|
|
final publishing =
|
|
statusText == "1" || statusText == "true" || (msg.number ?? 0) > 0;
|
|
updateRoomMusicPublishingState(userId: userId, publishing: publishing);
|
|
}
|
|
|
|
void publishCurrentUserRoomMusicState(bool publishing) {
|
|
final currentUser = AccountStorage().getCurrentUser()?.userProfile;
|
|
final currentUserId = (currentUser?.id ?? "").trim();
|
|
if (currentUserId.isEmpty) {
|
|
return;
|
|
}
|
|
|
|
final effectivePublishing = publishing && isOnMai();
|
|
updateRoomMusicPublishingState(
|
|
userId: currentUserId,
|
|
publishing: effectivePublishing,
|
|
);
|
|
|
|
final groupId =
|
|
currenRoom?.roomProfile?.roomProfile?.roomAccount?.trim() ?? "";
|
|
if (groupId.isEmpty) {
|
|
return;
|
|
}
|
|
if (rtmProvider == null && context != null && context!.mounted) {
|
|
rtmProvider = Provider.of<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,
|
|
);
|
|
}
|
|
}
|