3909 lines
120 KiB
Dart
3909 lines
120 KiB
Dart
import 'dart:async';
|
||
import 'package:dio/dio.dart';
|
||
|
||
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||
import 'package:yumi/app_localizations.dart';
|
||
import 'package:yumi/app/constants/sc_room_msg_type.dart';
|
||
import 'package:yumi/shared/tools/sc_permission_utils.dart';
|
||
import 'package:yumi/shared/tools/sc_room_utils.dart';
|
||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
|
||
import 'package:yumi/shared/data_sources/sources/repositories/sc_vip_repository_imp.dart';
|
||
import 'package:yumi/services/room/rc_room_manager.dart';
|
||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||
import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart';
|
||
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
|
||
import 'package:provider/provider.dart';
|
||
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||
import 'package:yumi/app/routes/sc_routes.dart';
|
||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||
import 'package:yumi/shared/tools/sc_lk_dialog_util.dart';
|
||
import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart';
|
||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
||
import 'package:yumi/shared/business_logic/models/res/join_room_res.dart';
|
||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||
import 'package:yumi/shared/business_logic/models/res/mic_res.dart';
|
||
import 'package:yumi/shared/business_logic/models/res/follow_room_res.dart'
|
||
as follow_room;
|
||
import 'package:yumi/shared/business_logic/models/res/my_room_res.dart'
|
||
as my_room;
|
||
import 'package:yumi/shared/business_logic/models/res/room_res.dart';
|
||
import 'package:yumi/modules/room/voice_room_route.dart';
|
||
import 'package:yumi/ui_kit/widgets/room/empty_mai_select.dart';
|
||
import 'package:yumi/ui_kit/widgets/room/room_user_info_card.dart';
|
||
import '../../shared/tools/sc_heartbeat_utils.dart';
|
||
import '../../shared/data_sources/models/enum/sc_heartbeat_status.dart';
|
||
import '../../shared/data_sources/models/enum/sc_room_info_event_type.dart';
|
||
import '../../shared/data_sources/models/enum/sc_room_roles_type.dart';
|
||
import '../../shared/business_logic/models/res/sc_is_follow_room_res.dart';
|
||
import '../../shared/business_logic/models/res/sc_room_red_packet_list_res.dart';
|
||
import '../../shared/business_logic/models/res/sc_room_rocket_status_res.dart';
|
||
import '../../shared/business_logic/models/res/sc_room_theme_list_res.dart';
|
||
import '../../shared/business_logic/models/res/sc_vip_res.dart';
|
||
import '../../shared/tools/sc_room_profile_cache.dart';
|
||
import '../../ui_kit/components/sc_float_ichart.dart';
|
||
|
||
typedef OnSoundVoiceChange = Function(num index, int volum);
|
||
typedef RtcProvider = RealTimeCommunicationManager;
|
||
typedef RoomMusicMixingStateListener =
|
||
void Function(AudioMixingStateType state, AudioMixingReasonType reason);
|
||
typedef RoomMusicMicRouteChangedListener = void Function(bool isOnMic);
|
||
|
||
enum RoomStartupStatus { idle, loading, ready, failed }
|
||
|
||
enum RoomStartupFailureType { none, entry, im, rtc }
|
||
|
||
class _RoomStartupAsyncResult<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;
|
||
}
|
||
}
|
||
|
||
class _AgoraJoinCanceled implements Exception {
|
||
const _AgoraJoinCanceled(this.reason);
|
||
|
||
final String reason;
|
||
|
||
@override
|
||
String toString() => reason;
|
||
}
|
||
|
||
enum _PendingRoomStartupSeatActionType { clickSeat, goUpMic }
|
||
|
||
class RoomEntryPreviewData {
|
||
const RoomEntryPreviewData({
|
||
this.roomId,
|
||
this.roomAccount,
|
||
this.userId,
|
||
this.roomCover,
|
||
this.roomBackground,
|
||
this.roomName,
|
||
this.roomDesc,
|
||
this.event,
|
||
this.sysOrigin,
|
||
this.countryCode,
|
||
this.countryName,
|
||
this.nationalFlag,
|
||
this.langCode,
|
||
this.del,
|
||
this.createTime,
|
||
this.updateTime,
|
||
this.activeTime,
|
||
this.roomSetting,
|
||
this.ownerProfile,
|
||
this.roomAdminCount,
|
||
this.roomMemberCount,
|
||
this.entrantsRole,
|
||
});
|
||
|
||
factory RoomEntryPreviewData.fromSocialChatRoom(SocialChatRoomRes room) {
|
||
return RoomEntryPreviewData(
|
||
roomId: room.id,
|
||
roomAccount: room.roomAccount,
|
||
userId: room.userId,
|
||
roomCover: room.roomCover,
|
||
roomBackground: room.roomBackground,
|
||
roomName: room.roomName,
|
||
roomDesc: room.roomDesc,
|
||
event: room.event,
|
||
sysOrigin: room.sysOrigin,
|
||
countryCode: room.countryCode,
|
||
countryName: room.countryName,
|
||
nationalFlag: room.nationalFlag,
|
||
roomSetting: room.extValues?.roomSetting,
|
||
ownerProfile: room.userProfile,
|
||
roomAdminCount: room.roomCounter?.adminCount,
|
||
roomMemberCount: room.roomCounter?.memberCount,
|
||
);
|
||
}
|
||
|
||
factory RoomEntryPreviewData.fromFollowRoom(follow_room.FollowRoomRes room) {
|
||
final roomProfile = room.roomProfile;
|
||
return RoomEntryPreviewData(
|
||
roomId: roomProfile?.id ?? room.id,
|
||
roomAccount: roomProfile?.roomAccount,
|
||
userId: roomProfile?.userId,
|
||
roomCover: roomProfile?.roomCover,
|
||
roomBackground: roomProfile?.roomBackground,
|
||
roomName: roomProfile?.roomName,
|
||
roomDesc: roomProfile?.roomDesc,
|
||
event: roomProfile?.event,
|
||
sysOrigin: roomProfile?.sysOrigin,
|
||
countryCode: roomProfile?.countryCode,
|
||
countryName: roomProfile?.countryName,
|
||
nationalFlag: roomProfile?.nationalFlag,
|
||
roomSetting: roomProfile?.extValues?.roomSetting,
|
||
ownerProfile: roomProfile?.userProfile,
|
||
roomAdminCount: roomProfile?.roomCounter?.adminCount,
|
||
roomMemberCount: roomProfile?.roomCounter?.memberCount,
|
||
);
|
||
}
|
||
|
||
factory RoomEntryPreviewData.fromMyRoom(
|
||
my_room.MyRoomRes room, {
|
||
SocialChatUserProfile? ownerProfile,
|
||
}) {
|
||
return RoomEntryPreviewData(
|
||
roomId: room.id,
|
||
roomAccount: room.roomAccount,
|
||
userId: room.userId,
|
||
roomCover: room.roomCover,
|
||
roomBackground: room.roomBackground,
|
||
roomName: room.roomName,
|
||
roomDesc: room.roomDesc,
|
||
event: room.event,
|
||
sysOrigin: room.sysOrigin,
|
||
countryCode: room.countryCode,
|
||
countryName: room.countryName,
|
||
nationalFlag: room.nationalFlag,
|
||
langCode: room.langCode,
|
||
del: room.del,
|
||
createTime: room.createTime,
|
||
updateTime: room.updateTime,
|
||
activeTime: room.activeTime,
|
||
roomSetting: room.setting,
|
||
ownerProfile: ownerProfile,
|
||
roomAdminCount: room.counter?.adminCount,
|
||
roomMemberCount: room.counter?.memberCount,
|
||
entrantsRole: SCRoomRolesType.HOMEOWNER.name,
|
||
);
|
||
}
|
||
|
||
final String? roomId;
|
||
final String? roomAccount;
|
||
final String? userId;
|
||
final String? roomCover;
|
||
final String? roomBackground;
|
||
final String? roomName;
|
||
final String? roomDesc;
|
||
final String? event;
|
||
final String? sysOrigin;
|
||
final String? countryCode;
|
||
final String? countryName;
|
||
final String? nationalFlag;
|
||
final String? langCode;
|
||
final bool? del;
|
||
final num? createTime;
|
||
final num? updateTime;
|
||
final num? activeTime;
|
||
final RoomSetting? roomSetting;
|
||
final SocialChatUserProfile? ownerProfile;
|
||
final num? roomAdminCount;
|
||
final num? roomMemberCount;
|
||
final String? entrantsRole;
|
||
|
||
int? get seatCount => roomSetting?.mikeSize?.toInt();
|
||
|
||
JoinRoomRes toJoinRoomRes({String? fallbackRoomId}) {
|
||
final resolvedRoomId =
|
||
(roomId ?? "").trim().isNotEmpty ? roomId : fallbackRoomId;
|
||
return JoinRoomRes(
|
||
entrants:
|
||
(entrantsRole ?? "").trim().isNotEmpty
|
||
? Entrants(roles: entrantsRole)
|
||
: null,
|
||
roomProfile: RoomProfile(
|
||
regionCode: countryCode,
|
||
roomCounter:
|
||
roomAdminCount != null || roomMemberCount != null
|
||
? RoomCounter(
|
||
adminCount: roomAdminCount,
|
||
memberCount: roomMemberCount,
|
||
)
|
||
: null,
|
||
roomProfile: RoomProfile2(
|
||
activeTime: activeTime,
|
||
countryCode: countryCode,
|
||
countryName: countryName,
|
||
createTime: createTime,
|
||
del: del,
|
||
event: event,
|
||
id: resolvedRoomId,
|
||
langCode: langCode,
|
||
nationalFlag: nationalFlag,
|
||
roomAccount: roomAccount,
|
||
roomCover: roomCover,
|
||
roomBackground: roomBackground,
|
||
roomDesc: roomDesc,
|
||
roomName: roomName,
|
||
sysOrigin: sysOrigin,
|
||
updateTime: updateTime,
|
||
userId: userId,
|
||
),
|
||
roomSetting: roomSetting,
|
||
userProfile: ownerProfile,
|
||
userSVipLevel: ownerProfile?.vipLevel,
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _PendingRoomStartupSeatAction {
|
||
const _PendingRoomStartupSeatAction.clickSeat({
|
||
required this.index,
|
||
this.clickUser,
|
||
}) : type = _PendingRoomStartupSeatActionType.clickSeat,
|
||
eventType = null,
|
||
inviterId = null;
|
||
|
||
const _PendingRoomStartupSeatAction.goUpMic({
|
||
required this.index,
|
||
this.eventType,
|
||
this.inviterId,
|
||
}) : type = _PendingRoomStartupSeatActionType.goUpMic,
|
||
clickUser = null;
|
||
|
||
final _PendingRoomStartupSeatActionType type;
|
||
final num index;
|
||
final SocialChatUserProfile? clickUser;
|
||
final String? eventType;
|
||
final String? inviterId;
|
||
}
|
||
|
||
class RealTimeCommunicationManager extends ChangeNotifier {
|
||
static const Duration _micListPollingInterval = Duration(seconds: 2);
|
||
static const Duration _onlineUsersPollingInterval = Duration(seconds: 3);
|
||
static const int _exitNetworkRetryLimit = 3;
|
||
static const Duration _exitNetworkRetryDelay = Duration(milliseconds: 500);
|
||
static const Duration _selfMicStateGracePeriod = Duration(seconds: 4);
|
||
static const Duration _giftTriggeredMicRefreshMinInterval = Duration(
|
||
milliseconds: 900,
|
||
);
|
||
static const Duration _agoraJoinTimeout = Duration(seconds: 8);
|
||
static const Duration _agoraDisconnectedGracePeriod = Duration(seconds: 12);
|
||
static const String _roomStartupSeatLoadingTag =
|
||
'roomStartupSeatInteractionLoading';
|
||
static const String _selfMicGoUpRetryLoadingTag = 'selfMicGoUpRetryLoading';
|
||
static const int _selfMicGoUpFailureLimit = 3;
|
||
|
||
bool needUpDataUserInfo = false;
|
||
bool _roomVisualEffectsEnabled = false;
|
||
bool _isExitingCurrentVoiceRoomSession = false;
|
||
bool _isHandlingAgoraRoomFailure = false;
|
||
bool _agoraJoined = false;
|
||
bool _agoraRoleIsBroadcaster = false;
|
||
String? _agoraJoinedChannelId;
|
||
bool _agoraAudioConfigured = false;
|
||
RoomStartupStatus _roomStartupStatus = RoomStartupStatus.idle;
|
||
RoomStartupFailureType _roomStartupFailureType = RoomStartupFailureType.none;
|
||
int _roomStartupFailureToken = 0;
|
||
int _roomEntryRequestSerial = 0;
|
||
int? _previewRoomSeatCount;
|
||
bool _currentRoomIsEntryPreview = false;
|
||
bool _isHandlingRoomStartupFailure = false;
|
||
Timer? _micListPollingTimer;
|
||
Timer? _onlineUsersPollingTimer;
|
||
Timer? _joinAgoraTimeoutTimer;
|
||
Timer? _agoraDisconnectedCleanupTimer;
|
||
Timer? _roomEntryEffectTimer;
|
||
Completer<void>? _joinAgoraCompleter;
|
||
Future<RtcEngine>? _rtcEngineInitTask;
|
||
Future<void>? _rtcEnginePrewarmTask;
|
||
Future<void>? _pendingRoomSwitchAgoraLeaveTask;
|
||
String? _joiningChannelId;
|
||
bool _isRefreshingMicList = false;
|
||
bool _isRefreshingOnlineUsers = false;
|
||
bool _disableMicListRefreshForCurrentSession = false;
|
||
bool _disableOnlineUsersRefreshForCurrentSession = false;
|
||
bool _isSelfMicActionInFlight = false;
|
||
bool _roomStartupSeatLoadingVisible = false;
|
||
bool _selfMicGoUpRetryLoadingVisible = false;
|
||
int _selfMicGoUpConsecutiveFailureCount = 0;
|
||
_PendingRoomStartupSeatAction? _pendingRoomStartupSeatAction;
|
||
bool _pendingMicListRefresh = false;
|
||
bool _pendingMicListRefreshNotifyIfUnchanged = false;
|
||
Timer? _deferredMicListRefreshTimer;
|
||
int _lastMicListRefreshStartedAtMs = 0;
|
||
num? _preferredSelfMicIndex;
|
||
num? _pendingSelfMicSourceIndex;
|
||
int? _pendingSelfMicSwitchGuardUntilMs;
|
||
num? _pendingSelfMicReleaseIndex;
|
||
int? _pendingSelfMicReleaseGuardUntilMs;
|
||
ClientRoleType? _lastAppliedClientRole;
|
||
bool? _lastAppliedLocalAudioMuted;
|
||
bool? _lastScheduledVoiceLiveOnMic;
|
||
String? _lastScheduledVoiceLiveRoomId;
|
||
String? _lastScheduledAnchorRoomId;
|
||
|
||
///当前所在房间
|
||
JoinRoomRes? currenRoom;
|
||
|
||
/// 声音音量变化监听
|
||
final List<OnSoundVoiceChange> _onSoundVoiceChangeList = [];
|
||
|
||
///麦位
|
||
Map<num, MicRes> roomWheatMap = {};
|
||
final Map<num, int> _seatEmojiEventVersions = {};
|
||
int _emojiPlaybackEventVersion = 0;
|
||
|
||
RtcEngine? engine;
|
||
RtcEngineEventHandler? _rtcEngineEventHandler;
|
||
RoomMusicMixingStateListener? _roomMusicMixingStateListener;
|
||
RoomMusicMicRouteChangedListener? _roomMusicMicRouteChangedListener;
|
||
BuildContext? context;
|
||
RtmProvider? rtmProvider;
|
||
|
||
SCIsFollowRoomRes? isFollowRoomRes;
|
||
SCRoomRocketStatusRes? roomRocketStatus;
|
||
|
||
///房间是否静音
|
||
bool roomIsMute = false;
|
||
|
||
bool closeFullGame = false;
|
||
|
||
///禁音开关 默认关闭
|
||
bool isMic = true;
|
||
|
||
bool shouldShowSeatSoundWave(num index) {
|
||
final seat = roomWheatMap[index];
|
||
final userId = (seat?.user?.id ?? "").trim();
|
||
if (userId.isEmpty || (seat?.micMute ?? false)) {
|
||
return false;
|
||
}
|
||
|
||
final currentUserId =
|
||
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
|
||
if (userId == currentUserId && isMic) {
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
///在线用户列表
|
||
List<SocialChatUserProfile> onlineUsers = [];
|
||
|
||
///房间管理员
|
||
List<SocialChatUserProfile> managerUsers = [];
|
||
|
||
///音乐是否正在播放
|
||
bool isMusicPlaying = false;
|
||
|
||
///房间红包列表
|
||
List<SCRoomRedPacketListRes> redPacketList = [];
|
||
|
||
num roomTaskClaimableCount = 0;
|
||
|
||
initializeRealTimeCommunicationManager(BuildContext context) {
|
||
this.context = context;
|
||
}
|
||
|
||
bool get roomVisualEffectsEnabled => _roomVisualEffectsEnabled;
|
||
|
||
bool get isExitingCurrentVoiceRoomSession =>
|
||
_isExitingCurrentVoiceRoomSession;
|
||
|
||
RoomStartupStatus get roomStartupStatus => _roomStartupStatus;
|
||
|
||
RoomStartupFailureType get roomStartupFailureType => _roomStartupFailureType;
|
||
|
||
int get roomStartupFailureToken => _roomStartupFailureToken;
|
||
|
||
int? get previewRoomSeatCount => _previewRoomSeatCount;
|
||
|
||
bool get shouldShowRoomVisualEffects =>
|
||
currenRoom != null && _roomVisualEffectsEnabled;
|
||
|
||
bool get _shouldDeferSeatInteractionForRoomStartup =>
|
||
_roomStartupStatus == RoomStartupStatus.loading;
|
||
|
||
void setRoomVisualEffectsEnabled(bool enabled) {
|
||
if (_roomVisualEffectsEnabled == enabled) {
|
||
return;
|
||
}
|
||
if (!enabled) {
|
||
_roomEntryEffectTimer?.cancel();
|
||
_roomEntryEffectTimer = null;
|
||
}
|
||
_roomVisualEffectsEnabled = enabled;
|
||
notifyListeners();
|
||
}
|
||
|
||
void _setRoomStartupLoading() {
|
||
_isHandlingRoomStartupFailure = false;
|
||
_roomStartupFailureType = RoomStartupFailureType.none;
|
||
if (_roomStartupStatus == RoomStartupStatus.loading) {
|
||
return;
|
||
}
|
||
_roomStartupStatus = RoomStartupStatus.loading;
|
||
notifyListeners();
|
||
}
|
||
|
||
void _setRoomStartupReady() {
|
||
_isHandlingRoomStartupFailure = false;
|
||
_roomStartupFailureType = RoomStartupFailureType.none;
|
||
if (_roomStartupStatus == RoomStartupStatus.ready) {
|
||
return;
|
||
}
|
||
_roomStartupStatus = RoomStartupStatus.ready;
|
||
notifyListeners();
|
||
_finishRoomStartupSeatLoading(executePendingAction: true);
|
||
}
|
||
|
||
void _setRoomStartupFailed(RoomStartupFailureType failureType) {
|
||
if (_roomStartupStatus != RoomStartupStatus.loading) {
|
||
return;
|
||
}
|
||
_stopRoomStatePolling();
|
||
_isHandlingRoomStartupFailure = false;
|
||
_roomStartupFailureType = failureType;
|
||
_roomStartupFailureToken += 1;
|
||
_roomStartupStatus = RoomStartupStatus.failed;
|
||
_finishRoomStartupSeatLoading(executePendingAction: false);
|
||
notifyListeners();
|
||
}
|
||
|
||
void _setExitingCurrentVoiceRoomSession(bool enabled, {bool notify = true}) {
|
||
if (_isExitingCurrentVoiceRoomSession == enabled) {
|
||
return;
|
||
}
|
||
_isExitingCurrentVoiceRoomSession = enabled;
|
||
if (notify) {
|
||
notifyListeners();
|
||
}
|
||
}
|
||
|
||
void _deferRoomStartupSeatAction(_PendingRoomStartupSeatAction action) {
|
||
_pendingRoomStartupSeatAction = action;
|
||
_showRoomStartupSeatLoading();
|
||
}
|
||
|
||
void _showRoomStartupSeatLoading() {
|
||
if (_roomStartupSeatLoadingVisible) {
|
||
return;
|
||
}
|
||
_roomStartupSeatLoadingVisible = true;
|
||
SmartDialog.dismiss(tag: _roomStartupSeatLoadingTag);
|
||
SmartDialog.show(
|
||
tag: _roomStartupSeatLoadingTag,
|
||
alignment: Alignment.center,
|
||
animationType: SmartAnimationType.fade,
|
||
backType: SmartBackType.block,
|
||
clickMaskDismiss: false,
|
||
maskColor: Colors.transparent,
|
||
builder: (_) {
|
||
return Center(
|
||
child: Container(
|
||
width: 55,
|
||
height: 55,
|
||
decoration: BoxDecoration(
|
||
color: Colors.black26,
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: const Center(child: SCRotatingDotsLoading(size: 28)),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
void _showSelfMicGoUpRetryLoading() {
|
||
if (_selfMicGoUpRetryLoadingVisible) {
|
||
return;
|
||
}
|
||
_selfMicGoUpRetryLoadingVisible = true;
|
||
SmartDialog.dismiss(tag: _selfMicGoUpRetryLoadingTag);
|
||
SmartDialog.show(
|
||
tag: _selfMicGoUpRetryLoadingTag,
|
||
alignment: Alignment.center,
|
||
animationType: SmartAnimationType.fade,
|
||
backType: SmartBackType.normal,
|
||
clickMaskDismiss: true,
|
||
maskColor: Colors.transparent,
|
||
onDismiss: () {
|
||
_selfMicGoUpRetryLoadingVisible = false;
|
||
},
|
||
builder: (_) {
|
||
return Center(
|
||
child: Container(
|
||
width: 55,
|
||
height: 55,
|
||
decoration: BoxDecoration(
|
||
color: Colors.black26,
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: const Center(child: SCRotatingDotsLoading(size: 28)),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
void _dismissSelfMicGoUpRetryLoading() {
|
||
if (!_selfMicGoUpRetryLoadingVisible) {
|
||
return;
|
||
}
|
||
_selfMicGoUpRetryLoadingVisible = false;
|
||
SmartDialog.dismiss(tag: _selfMicGoUpRetryLoadingTag);
|
||
}
|
||
|
||
void _resetSelfMicGoUpFailureState() {
|
||
_selfMicGoUpConsecutiveFailureCount = 0;
|
||
_dismissSelfMicGoUpRetryLoading();
|
||
}
|
||
|
||
void _handleSelfMicGoUpFailure(String message) {
|
||
_selfMicGoUpConsecutiveFailureCount += 1;
|
||
if (_selfMicGoUpConsecutiveFailureCount >= _selfMicGoUpFailureLimit) {
|
||
SCTts.show(
|
||
'Failed to put on the microphone after multiple attempts, please try again later',
|
||
);
|
||
return;
|
||
}
|
||
SCTts.show(message);
|
||
}
|
||
|
||
void _finishRoomStartupSeatLoading({required bool executePendingAction}) {
|
||
final pendingAction = _pendingRoomStartupSeatAction;
|
||
_pendingRoomStartupSeatAction = null;
|
||
if (_roomStartupSeatLoadingVisible) {
|
||
_roomStartupSeatLoadingVisible = false;
|
||
SmartDialog.dismiss(tag: _roomStartupSeatLoadingTag);
|
||
}
|
||
if (!executePendingAction || pendingAction == null) {
|
||
return;
|
||
}
|
||
Future<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');
|
||
}
|
||
|
||
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));
|
||
}
|
||
|
||
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;
|
||
}
|
||
return _stabilizeSelfMicSnapshot(nextMap, previousMap: previousMap);
|
||
}
|
||
|
||
void _startSelfMicSwitchGuard({
|
||
required num sourceIndex,
|
||
required num targetIndex,
|
||
}) {
|
||
_clearSelfMicReleaseGuard();
|
||
_preferredSelfMicIndex = targetIndex;
|
||
_pendingSelfMicSourceIndex = sourceIndex;
|
||
_pendingSelfMicSwitchGuardUntilMs =
|
||
DateTime.now().add(_selfMicStateGracePeriod).millisecondsSinceEpoch;
|
||
}
|
||
|
||
void _clearSelfMicSwitchGuard({bool clearPreferredIndex = false}) {
|
||
_pendingSelfMicSourceIndex = null;
|
||
_pendingSelfMicSwitchGuardUntilMs = null;
|
||
if (clearPreferredIndex) {
|
||
_preferredSelfMicIndex = null;
|
||
}
|
||
}
|
||
|
||
void _startSelfMicReleaseGuard({required num sourceIndex}) {
|
||
_pendingSelfMicReleaseIndex = sourceIndex;
|
||
_pendingSelfMicReleaseGuardUntilMs =
|
||
DateTime.now().add(_selfMicStateGracePeriod).millisecondsSinceEpoch;
|
||
}
|
||
|
||
void _clearSelfMicReleaseGuard() {
|
||
_pendingSelfMicReleaseIndex = null;
|
||
_pendingSelfMicReleaseGuardUntilMs = null;
|
||
}
|
||
|
||
bool _shouldSuppressCurrentUserOnSeat(num index, {MicRes? seat}) {
|
||
final currentUserId =
|
||
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
|
||
if (currentUserId.isEmpty) {
|
||
return false;
|
||
}
|
||
final releaseIndex = _pendingSelfMicReleaseIndex;
|
||
final releaseGuardUntilMs = _pendingSelfMicReleaseGuardUntilMs ?? 0;
|
||
if (releaseIndex != index ||
|
||
releaseGuardUntilMs <= DateTime.now().millisecondsSinceEpoch) {
|
||
return false;
|
||
}
|
||
final resolvedSeat = seat ?? roomWheatMap[index];
|
||
return (resolvedSeat?.user?.id ?? "").trim() == currentUserId;
|
||
}
|
||
|
||
MicRes? micAtIndexForDisplay(num index) {
|
||
final seat = roomWheatMap[index];
|
||
if (!_shouldSuppressCurrentUserOnSeat(index, seat: seat)) {
|
||
return seat;
|
||
}
|
||
return seat?.copyWith(clearUser: true);
|
||
}
|
||
|
||
int emojiEventVersionForSeat(num index) {
|
||
return _seatEmojiEventVersions[index] ?? 0;
|
||
}
|
||
|
||
void consumeSeatEmojiEvent(num index, int eventVersion) {
|
||
if (emojiEventVersionForSeat(index) != eventVersion) {
|
||
return;
|
||
}
|
||
_seatEmojiEventVersions.remove(index);
|
||
roomWheatMap[index]?.setEmojiPath = null;
|
||
}
|
||
|
||
Map<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();
|
||
return nextMap;
|
||
}
|
||
|
||
if (!hasActiveGuard) {
|
||
_clearSelfMicSwitchGuard();
|
||
} else {
|
||
final resolvedTargetIndex = targetIndex;
|
||
final resolvedSourceIndex = sourceIndex;
|
||
|
||
final selfOnlyOnSource =
|
||
selfSeatIndices.isEmpty ||
|
||
selfSeatIndices.every(
|
||
(seatIndex) => seatIndex == resolvedSourceIndex,
|
||
);
|
||
if (selfOnlyOnSource) {
|
||
final optimisticTargetSeat = previousMap[resolvedTargetIndex];
|
||
if (optimisticTargetSeat != null &&
|
||
optimisticTargetSeat.user?.id == currentUserId) {
|
||
final incomingTargetSeat = nextMap[resolvedTargetIndex];
|
||
if (incomingTargetSeat?.user == null ||
|
||
incomingTargetSeat?.user?.id == currentUserId) {
|
||
final stabilizedMap = Map<num, MicRes>.from(nextMap);
|
||
for (final seatIndex in selfSeatIndices) {
|
||
if (seatIndex == resolvedTargetIndex) {
|
||
continue;
|
||
}
|
||
final seat = stabilizedMap[seatIndex];
|
||
if (seat == null) {
|
||
continue;
|
||
}
|
||
stabilizedMap[seatIndex] = seat.copyWith(clearUser: true);
|
||
}
|
||
|
||
final baseSeat = incomingTargetSeat ?? optimisticTargetSeat;
|
||
stabilizedMap[resolvedTargetIndex] = baseSeat.copyWith(
|
||
user: optimisticTargetSeat.user,
|
||
micMute:
|
||
incomingTargetSeat?.micMute ?? optimisticTargetSeat.micMute,
|
||
micLock:
|
||
incomingTargetSeat?.micLock ?? optimisticTargetSeat.micLock,
|
||
roomToken:
|
||
incomingTargetSeat?.roomToken ??
|
||
optimisticTargetSeat.roomToken,
|
||
emojiPath:
|
||
(incomingTargetSeat?.emojiPath ?? "").isNotEmpty
|
||
? incomingTargetSeat?.emojiPath
|
||
: optimisticTargetSeat.emojiPath,
|
||
type:
|
||
(incomingTargetSeat?.type ?? "").isNotEmpty
|
||
? incomingTargetSeat?.type
|
||
: optimisticTargetSeat.type,
|
||
number:
|
||
(incomingTargetSeat?.number ?? "").isNotEmpty
|
||
? incomingTargetSeat?.number
|
||
: optimisticTargetSeat.number,
|
||
);
|
||
|
||
return stabilizedMap;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
final releaseIndex = _pendingSelfMicReleaseIndex;
|
||
final releaseGuardUntilMs = _pendingSelfMicReleaseGuardUntilMs ?? 0;
|
||
final hasActiveReleaseGuard =
|
||
releaseIndex != null && releaseGuardUntilMs > nowMs;
|
||
if (!hasActiveReleaseGuard) {
|
||
_clearSelfMicReleaseGuard();
|
||
return nextMap;
|
||
}
|
||
|
||
if (selfSeatIndices.isEmpty) {
|
||
return nextMap;
|
||
}
|
||
|
||
if (selfSeatIndices.any((seatIndex) => seatIndex != releaseIndex)) {
|
||
_clearSelfMicReleaseGuard();
|
||
return nextMap;
|
||
}
|
||
|
||
final releaseSeat = nextMap[releaseIndex];
|
||
if (releaseSeat?.user?.id != currentUserId) {
|
||
_clearSelfMicReleaseGuard();
|
||
return nextMap;
|
||
}
|
||
|
||
final stabilizedMap = Map<num, MicRes>.from(nextMap);
|
||
stabilizedMap[releaseIndex] = releaseSeat!.copyWith(clearUser: true);
|
||
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;
|
||
}
|
||
|
||
bool _isCurrentAgoraChannel(String? channelId) {
|
||
final normalizedChannelId = (channelId ?? "").trim();
|
||
final currentRoomId =
|
||
(currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
|
||
if (normalizedChannelId.isEmpty) {
|
||
return false;
|
||
}
|
||
return normalizedChannelId == currentRoomId ||
|
||
normalizedChannelId == (_joiningChannelId ?? "").trim();
|
||
}
|
||
|
||
void _cancelAgoraJoinWaiter([Object? error]) {
|
||
_joinAgoraTimeoutTimer?.cancel();
|
||
_joinAgoraTimeoutTimer = null;
|
||
final completer = _joinAgoraCompleter;
|
||
_joinAgoraCompleter = null;
|
||
if (error != null && completer != null && !completer.isCompleted) {
|
||
completer.completeError(error);
|
||
}
|
||
}
|
||
|
||
void _cancelAgoraDisconnectedCleanup() {
|
||
_agoraDisconnectedCleanupTimer?.cancel();
|
||
_agoraDisconnectedCleanupTimer = null;
|
||
}
|
||
|
||
void _resetAgoraTracking({bool clearJoiningChannel = true}) {
|
||
_cancelAgoraJoinWaiter(const _AgoraJoinCanceled('Agora join canceled'));
|
||
_cancelAgoraDisconnectedCleanup();
|
||
_agoraJoined = false;
|
||
_agoraRoleIsBroadcaster = false;
|
||
_agoraJoinedChannelId = null;
|
||
if (clearJoiningChannel) {
|
||
_joiningChannelId = null;
|
||
}
|
||
_resetLocalAudioRuntimeTracking();
|
||
}
|
||
|
||
bool get _canReportMicActive {
|
||
return _agoraJoined && _agoraRoleIsBroadcaster && isOnMai();
|
||
}
|
||
|
||
bool get _joinAgoraCompleterIsPending {
|
||
final completer = _joinAgoraCompleter;
|
||
return completer != null && !completer.isCompleted;
|
||
}
|
||
|
||
void _resyncHeartbeatFromAgoraState() {
|
||
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||
final canReportMicActive = _canReportMicActive;
|
||
_syncRoomHeartbeatState(
|
||
roomId: roomId,
|
||
isOnMic: canReportMicActive,
|
||
shouldRunAnchorHeartbeat: canReportMicActive,
|
||
);
|
||
}
|
||
|
||
bool _isFatalAgoraError(ErrorCodeType err) {
|
||
return err == ErrorCodeType.errInvalidArgument ||
|
||
err == ErrorCodeType.errNotReady ||
|
||
err == ErrorCodeType.errNotSupported ||
|
||
err == ErrorCodeType.errRefused ||
|
||
err == ErrorCodeType.errInvalidState ||
|
||
err == ErrorCodeType.errNoPermission ||
|
||
err == ErrorCodeType.errJoinChannelRejected ||
|
||
err == ErrorCodeType.errInvalidAppId ||
|
||
err == ErrorCodeType.errInvalidChannelName ||
|
||
err == ErrorCodeType.errTokenExpired ||
|
||
err == ErrorCodeType.errInvalidToken ||
|
||
err == ErrorCodeType.errConnectionLost;
|
||
}
|
||
|
||
bool _isFatalConnectionReason(ConnectionChangedReasonType reason) {
|
||
return reason ==
|
||
ConnectionChangedReasonType.connectionChangedBannedByServer ||
|
||
reason == ConnectionChangedReasonType.connectionChangedJoinFailed ||
|
||
reason == ConnectionChangedReasonType.connectionChangedInvalidAppId ||
|
||
reason ==
|
||
ConnectionChangedReasonType.connectionChangedInvalidChannelName ||
|
||
reason == ConnectionChangedReasonType.connectionChangedInvalidToken ||
|
||
reason == ConnectionChangedReasonType.connectionChangedTokenExpired ||
|
||
reason ==
|
||
ConnectionChangedReasonType.connectionChangedRejectedByServer ||
|
||
reason == ConnectionChangedReasonType.connectionChangedSameUidLogin ||
|
||
reason ==
|
||
ConnectionChangedReasonType.connectionChangedTooManyBroadcasters ||
|
||
reason ==
|
||
ConnectionChangedReasonType
|
||
.connectionChangedLicenseValidationFailure;
|
||
}
|
||
|
||
void _syncSelfMicRuntimeState() {
|
||
final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id;
|
||
if ((currentUserId ?? "").isEmpty) {
|
||
return;
|
||
}
|
||
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||
|
||
MicRes? currentUserMic;
|
||
for (final mic in roomWheatMap.values) {
|
||
if (mic.user?.id == currentUserId) {
|
||
currentUserMic = mic;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (currentUserMic == null) {
|
||
_clearSelfMicSwitchGuard(clearPreferredIndex: true);
|
||
_agoraRoleIsBroadcaster = false;
|
||
_syncRoomHeartbeatState(
|
||
roomId: roomId,
|
||
isOnMic: false,
|
||
shouldRunAnchorHeartbeat: false,
|
||
);
|
||
_applyLocalAudioRuntimeState(
|
||
clientRole: ClientRoleType.clientRoleAudience,
|
||
muted: true,
|
||
);
|
||
_roomMusicMicRouteChangedListener?.call(false);
|
||
return;
|
||
}
|
||
|
||
_preferredSelfMicIndex = currentUserMic.micIndex;
|
||
final canReportMicActive = _canReportMicActive;
|
||
_syncRoomHeartbeatState(
|
||
roomId: roomId,
|
||
isOnMic: canReportMicActive,
|
||
shouldRunAnchorHeartbeat: canReportMicActive,
|
||
);
|
||
|
||
_applyLocalAudioRuntimeState(
|
||
clientRole: ClientRoleType.clientRoleBroadcaster,
|
||
muted: (currentUserMic.micMute ?? false) || isMic,
|
||
);
|
||
_roomMusicMicRouteChangedListener?.call(true);
|
||
}
|
||
|
||
void _syncRoomHeartbeatState({
|
||
required String roomId,
|
||
required bool isOnMic,
|
||
required bool shouldRunAnchorHeartbeat,
|
||
}) {
|
||
final normalizedRoomId = roomId.trim();
|
||
if (normalizedRoomId.isEmpty) {
|
||
_resetHeartbeatTracking();
|
||
return;
|
||
}
|
||
|
||
if (!_agoraJoined) {
|
||
if (_lastScheduledVoiceLiveRoomId != null ||
|
||
_lastScheduledAnchorRoomId != null) {
|
||
SCHeartbeatUtils.scheduleHeartbeat(
|
||
SCHeartbeatStatus.ONLINE.name,
|
||
false,
|
||
);
|
||
SCHeartbeatUtils.cancelAnchorTimer();
|
||
}
|
||
_resetHeartbeatTracking();
|
||
return;
|
||
}
|
||
|
||
final voiceLiveChanged =
|
||
_lastScheduledVoiceLiveRoomId != normalizedRoomId ||
|
||
_lastScheduledVoiceLiveOnMic != isOnMic;
|
||
if (voiceLiveChanged) {
|
||
SCHeartbeatUtils.scheduleHeartbeat(
|
||
SCHeartbeatStatus.VOICE_LIVE.name,
|
||
isOnMic,
|
||
roomId: normalizedRoomId,
|
||
);
|
||
_lastScheduledVoiceLiveRoomId = normalizedRoomId;
|
||
_lastScheduledVoiceLiveOnMic = isOnMic;
|
||
}
|
||
|
||
if (!shouldRunAnchorHeartbeat) {
|
||
if (_lastScheduledAnchorRoomId != null) {
|
||
SCHeartbeatUtils.cancelAnchorTimer();
|
||
_lastScheduledAnchorRoomId = null;
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (voiceLiveChanged || _lastScheduledAnchorRoomId != normalizedRoomId) {
|
||
SCHeartbeatUtils.scheduleAnchorHeartbeat(normalizedRoomId);
|
||
_lastScheduledAnchorRoomId = normalizedRoomId;
|
||
}
|
||
}
|
||
|
||
Future<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 ClientRoleType clientRole,
|
||
required bool muted,
|
||
}) {
|
||
if (clientRole != ClientRoleType.clientRoleBroadcaster) {
|
||
_agoraRoleIsBroadcaster = false;
|
||
}
|
||
if (engine == null) {
|
||
_lastAppliedClientRole = null;
|
||
_lastAppliedLocalAudioMuted = null;
|
||
return;
|
||
}
|
||
|
||
if (_lastAppliedClientRole != clientRole) {
|
||
engine?.setClientRole(role: clientRole);
|
||
_lastAppliedClientRole = clientRole;
|
||
}
|
||
if (!muted && _lastAppliedLocalAudioMuted != false) {
|
||
adjustRecordingSignalVolume(100);
|
||
}
|
||
if (_lastAppliedLocalAudioMuted != muted) {
|
||
engine?.muteLocalAudioStream(muted);
|
||
_lastAppliedLocalAudioMuted = muted;
|
||
}
|
||
}
|
||
|
||
void _resetHeartbeatTracking() {
|
||
_lastScheduledVoiceLiveOnMic = null;
|
||
_lastScheduledVoiceLiveRoomId = null;
|
||
_lastScheduledAnchorRoomId = null;
|
||
}
|
||
|
||
void _resetLocalAudioRuntimeTracking() {
|
||
_lastAppliedClientRole = null;
|
||
_lastAppliedLocalAudioMuted = null;
|
||
}
|
||
|
||
void _applyMicSnapshot(
|
||
Map<num, MicRes> nextMap, {
|
||
bool notifyIfUnchanged = true,
|
||
}) {
|
||
final changed = !_sameMicMaps(roomWheatMap, nextMap);
|
||
roomWheatMap = nextMap;
|
||
_syncSelfMicRuntimeState();
|
||
if (changed || notifyIfUnchanged) {
|
||
notifyListeners();
|
||
}
|
||
}
|
||
|
||
int? _tryParseAgoraUid(String? value) {
|
||
final normalizedValue = (value ?? '').trim();
|
||
if (normalizedValue.isEmpty) {
|
||
return null;
|
||
}
|
||
return int.tryParse(normalizedValue);
|
||
}
|
||
|
||
int _stableAgoraUidFromUser(SocialChatUserProfile? user) {
|
||
final source =
|
||
'${user?.account ?? ""}|${user?.id ?? ""}|${user?.userNickname ?? ""}';
|
||
var hash = 0x811C9DC5;
|
||
for (final codeUnit in source.codeUnits) {
|
||
hash ^= codeUnit;
|
||
hash = (hash * 0x01000193) & 0x7fffffff;
|
||
}
|
||
return hash == 0 ? 1 : hash;
|
||
}
|
||
|
||
int _resolveAgoraUidForUser(SocialChatUserProfile? user) {
|
||
final parsedAccountUid = _tryParseAgoraUid(user?.account);
|
||
if (parsedAccountUid != null && parsedAccountUid > 0) {
|
||
return parsedAccountUid;
|
||
}
|
||
final parsedUserIdUid = _tryParseAgoraUid(user?.id);
|
||
if (parsedUserIdUid != null && parsedUserIdUid > 0) {
|
||
return parsedUserIdUid;
|
||
}
|
||
return _stableAgoraUidFromUser(user);
|
||
}
|
||
|
||
int _resolveAgoraUidForCurrentUser() {
|
||
return _resolveAgoraUidForUser(
|
||
AccountStorage().getCurrentUser()?.userProfile,
|
||
);
|
||
}
|
||
|
||
void requestGiftTriggeredMicRefresh() {
|
||
requestMicrophoneListRefresh(
|
||
notifyIfUnchanged: false,
|
||
minInterval: _giftTriggeredMicRefreshMinInterval,
|
||
);
|
||
}
|
||
|
||
void requestMicrophoneListRefresh({
|
||
bool notifyIfUnchanged = false,
|
||
Duration minInterval = Duration.zero,
|
||
}) {
|
||
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||
if (roomId.isEmpty || _disableMicListRefreshForCurrentSession) {
|
||
return;
|
||
}
|
||
|
||
if (minInterval <= Duration.zero) {
|
||
_deferredMicListRefreshTimer?.cancel();
|
||
_deferredMicListRefreshTimer = null;
|
||
retrieveMicrophoneList(
|
||
notifyIfUnchanged: notifyIfUnchanged,
|
||
).catchError((_) {});
|
||
return;
|
||
}
|
||
|
||
final nowMs = DateTime.now().millisecondsSinceEpoch;
|
||
final elapsedMs = nowMs - _lastMicListRefreshStartedAtMs;
|
||
if (!_isRefreshingMicList && elapsedMs >= minInterval.inMilliseconds) {
|
||
_deferredMicListRefreshTimer?.cancel();
|
||
_deferredMicListRefreshTimer = null;
|
||
retrieveMicrophoneList(
|
||
notifyIfUnchanged: notifyIfUnchanged,
|
||
).catchError((_) {});
|
||
return;
|
||
}
|
||
|
||
final remainingMs = minInterval.inMilliseconds - elapsedMs;
|
||
final delayMs = remainingMs > 80 ? remainingMs : 80;
|
||
_deferredMicListRefreshTimer?.cancel();
|
||
_deferredMicListRefreshTimer = Timer(Duration(milliseconds: delayMs), () {
|
||
_deferredMicListRefreshTimer = null;
|
||
retrieveMicrophoneList(
|
||
notifyIfUnchanged: notifyIfUnchanged,
|
||
).catchError((_) {});
|
||
});
|
||
}
|
||
|
||
Future<void> _cleanupAgoraRoomState({
|
||
bool clearMicSeats = false,
|
||
String? onlyIfJoinedChannelId,
|
||
}) async {
|
||
final expectedChannelId = (onlyIfJoinedChannelId ?? "").trim();
|
||
if (expectedChannelId.isNotEmpty) {
|
||
final joinedChannelId = (_agoraJoinedChannelId ?? "").trim();
|
||
final currentRoomId =
|
||
(currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
|
||
if (joinedChannelId.isNotEmpty && joinedChannelId != expectedChannelId) {
|
||
return;
|
||
}
|
||
if (joinedChannelId.isEmpty &&
|
||
currentRoomId.isNotEmpty &&
|
||
currentRoomId != expectedChannelId) {
|
||
return;
|
||
}
|
||
}
|
||
_resetAgoraTracking();
|
||
try {
|
||
await engine?.leaveChannel();
|
||
} catch (error) {
|
||
debugPrint('[Agora] leave channel during cleanup failed: $error');
|
||
}
|
||
if (clearMicSeats) {
|
||
roomWheatMap.clear();
|
||
notifyListeners();
|
||
}
|
||
_resyncHeartbeatFromAgoraState();
|
||
}
|
||
|
||
Future<void> _leaveAgoraRoomForExit() async {
|
||
final rtcEngine = engine;
|
||
_resetAgoraTracking();
|
||
if (rtcEngine == null) {
|
||
return;
|
||
}
|
||
try {
|
||
await rtcEngine.leaveChannel();
|
||
} catch (error) {
|
||
debugPrint('[RoomExit] leave Agora channel failed: $error');
|
||
}
|
||
}
|
||
|
||
Future<void> _handleAgoraJoinFailed(
|
||
Object error, {
|
||
required bool exitRoom,
|
||
}) async {
|
||
debugPrint('[Agora] join failed: $error');
|
||
await _cleanupAgoraRoomState(clearMicSeats: true);
|
||
if (!exitRoom || _isExitingCurrentVoiceRoomSession) {
|
||
return;
|
||
}
|
||
SCTts.show("Join room fail");
|
||
unawaited(exitCurrentVoiceRoomSession(false));
|
||
}
|
||
|
||
Future<void> _handleAgoraDisconnected(Object reason) async {
|
||
if (_isHandlingAgoraRoomFailure || _isExitingCurrentVoiceRoomSession) {
|
||
return;
|
||
}
|
||
_isHandlingAgoraRoomFailure = true;
|
||
debugPrint('[Agora] disconnected, leave room: $reason');
|
||
SCTts.show("Voice connection failed");
|
||
await _cleanupAgoraRoomState(clearMicSeats: true);
|
||
await exitCurrentVoiceRoomSession(false);
|
||
}
|
||
|
||
void _scheduleAgoraDisconnectedCleanup(Object reason) {
|
||
if (_isExitingCurrentVoiceRoomSession) {
|
||
return;
|
||
}
|
||
_agoraDisconnectedCleanupTimer?.cancel();
|
||
_agoraDisconnectedCleanupTimer = Timer(_agoraDisconnectedGracePeriod, () {
|
||
_agoraDisconnectedCleanupTimer = null;
|
||
if (!_agoraJoined && !_isExitingCurrentVoiceRoomSession) {
|
||
unawaited(_handleAgoraDisconnected(reason));
|
||
}
|
||
});
|
||
}
|
||
|
||
Future<void> prewarmRtcEngine() {
|
||
final existingTask = _rtcEnginePrewarmTask;
|
||
if (existingTask != null) {
|
||
return existingTask;
|
||
}
|
||
final task = _prewarmRtcEngine();
|
||
_rtcEnginePrewarmTask = task;
|
||
return task;
|
||
}
|
||
|
||
Future<void> _prewarmRtcEngine() async {
|
||
try {
|
||
final rtcEngine = await _initAgoraRtcEngine();
|
||
await _configureAgoraAudioEngine(rtcEngine);
|
||
} catch (error, stackTrace) {
|
||
_rtcEnginePrewarmTask = null;
|
||
debugPrint('[Agora] prewarm failed: $error\n$stackTrace');
|
||
rethrow;
|
||
}
|
||
}
|
||
|
||
Future<String> _fetchRtcTokenForCurrentRoom({
|
||
required bool isPublisher,
|
||
}) async {
|
||
final channelId = (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
|
||
final userId =
|
||
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
|
||
final rtcToken = await SCAccountRepository().getRtcToken(
|
||
channelId,
|
||
userId,
|
||
isPublisher: isPublisher,
|
||
);
|
||
return rtcToken.rtcToken ?? "";
|
||
}
|
||
|
||
Future<_RoomStartupAsyncResult<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> joinAgoraVoiceChannel({
|
||
bool throwOnError = false,
|
||
String? rtcToken,
|
||
}) async {
|
||
final channelId = (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
|
||
if (channelId.isEmpty) {
|
||
final error = StateError('Agora channel id is empty');
|
||
await _handleAgoraJoinFailed(error, exitRoom: !throwOnError);
|
||
if (throwOnError) {
|
||
throw error;
|
||
}
|
||
return;
|
||
}
|
||
|
||
final pendingRoomSwitchLeaveTask = _pendingRoomSwitchAgoraLeaveTask;
|
||
if (pendingRoomSwitchLeaveTask != null) {
|
||
try {
|
||
await pendingRoomSwitchLeaveTask;
|
||
} catch (error) {
|
||
debugPrint('[Agora] pending room switch leave failed: $error');
|
||
}
|
||
}
|
||
|
||
_cancelAgoraJoinWaiter(const _AgoraJoinCanceled('Agora join superseded'));
|
||
_cancelAgoraDisconnectedCleanup();
|
||
_joiningChannelId = channelId;
|
||
_agoraJoined = false;
|
||
_agoraRoleIsBroadcaster = false;
|
||
final joinCompleter = Completer<void>();
|
||
_joinAgoraCompleter = joinCompleter;
|
||
_joinAgoraTimeoutTimer = Timer(_agoraJoinTimeout, () {
|
||
if (!joinCompleter.isCompleted) {
|
||
joinCompleter.completeError(
|
||
TimeoutException('Agora join timeout', _agoraJoinTimeout),
|
||
);
|
||
}
|
||
});
|
||
|
||
try {
|
||
final rtcEngine = await _initAgoraRtcEngine();
|
||
await _configureAgoraAudioEngine(rtcEngine);
|
||
final resolvedRtcToken =
|
||
rtcToken ?? await _fetchRtcTokenForCurrentRoom(isPublisher: false);
|
||
await rtcEngine.joinChannel(
|
||
token: resolvedRtcToken,
|
||
channelId: channelId,
|
||
uid: _resolveAgoraUidForCurrentUser(),
|
||
options: const ChannelMediaOptions(
|
||
// 自动订阅所有视频流
|
||
autoSubscribeVideo: false,
|
||
// 自动订阅所有音频流
|
||
autoSubscribeAudio: true,
|
||
// 发布摄像头采集的视频
|
||
publishCameraTrack: false,
|
||
// 发布麦克风采集的音频
|
||
publishMicrophoneTrack: true,
|
||
// 设置用户角色为 clientRoleBroadcaster(主播)或 clientRoleAudience(观众)
|
||
clientRoleType: ClientRoleType.clientRoleAudience,
|
||
channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
|
||
),
|
||
);
|
||
await joinCompleter.future;
|
||
_syncSelfMicRuntimeState();
|
||
rtcEngine.muteAllRemoteAudioStreams(roomIsMute);
|
||
} catch (e) {
|
||
if (e is _AgoraJoinCanceled) {
|
||
debugPrint('[Agora] join canceled: $e');
|
||
if (throwOnError) {
|
||
rethrow;
|
||
}
|
||
return;
|
||
}
|
||
await _handleAgoraJoinFailed(e, exitRoom: !throwOnError);
|
||
if (throwOnError) {
|
||
rethrow;
|
||
}
|
||
debugPrint('加入失败:${e.runtimeType},${e.toString()}');
|
||
} finally {
|
||
if (identical(_joinAgoraCompleter, joinCompleter)) {
|
||
_cancelAgoraJoinWaiter();
|
||
}
|
||
}
|
||
}
|
||
|
||
Future<bool> _switchAgoraToBroadcaster({
|
||
required String publisherToken,
|
||
required bool muted,
|
||
}) async {
|
||
if (!_agoraJoined || engine == null) {
|
||
return false;
|
||
}
|
||
try {
|
||
if (publisherToken.trim().isNotEmpty) {
|
||
await engine?.renewToken(publisherToken);
|
||
}
|
||
await engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
|
||
if (!muted) {
|
||
adjustRecordingSignalVolume(100);
|
||
}
|
||
await engine?.muteLocalAudioStream(muted);
|
||
_lastAppliedClientRole = ClientRoleType.clientRoleBroadcaster;
|
||
_lastAppliedLocalAudioMuted = muted;
|
||
_agoraRoleIsBroadcaster = true;
|
||
_resyncHeartbeatFromAgoraState();
|
||
return true;
|
||
} catch (error) {
|
||
debugPrint('[Agora] switch to broadcaster failed: $error');
|
||
_agoraRoleIsBroadcaster = false;
|
||
_resyncHeartbeatFromAgoraState();
|
||
return false;
|
||
}
|
||
}
|
||
|
||
Future<bool> _switchAgoraToAudience() async {
|
||
try {
|
||
await engine?.setClientRole(role: ClientRoleType.clientRoleAudience);
|
||
await engine?.muteLocalAudioStream(true);
|
||
_lastAppliedClientRole = ClientRoleType.clientRoleAudience;
|
||
_lastAppliedLocalAudioMuted = true;
|
||
_agoraRoleIsBroadcaster = false;
|
||
_resyncHeartbeatFromAgoraState();
|
||
return true;
|
||
} catch (error) {
|
||
debugPrint('[Agora] switch to audience failed: $error');
|
||
return false;
|
||
}
|
||
}
|
||
|
||
Future<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 (_agoraJoined && isOnMai()) {
|
||
await _switchAgoraToBroadcaster(
|
||
publisherToken: seat?.roomToken ?? "",
|
||
muted: true,
|
||
);
|
||
}
|
||
} else {
|
||
await _switchAgoraToBroadcaster(
|
||
publisherToken: seat?.roomToken ?? "",
|
||
muted: false,
|
||
);
|
||
}
|
||
notifyListeners();
|
||
}
|
||
|
||
Future<void> handleSelfMicRemovedByRemote({
|
||
bool refreshMicList = true,
|
||
}) async {
|
||
final currentUserId =
|
||
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
|
||
if (currentUserId.isEmpty) {
|
||
await _switchAgoraToAudience();
|
||
return;
|
||
}
|
||
|
||
final currentSeatIndex = userOnMaiInIndex(currentUserId);
|
||
isMic = true;
|
||
_clearSelfMicSwitchGuard(clearPreferredIndex: true);
|
||
if (currentSeatIndex > -1) {
|
||
_startSelfMicReleaseGuard(sourceIndex: currentSeatIndex);
|
||
} else {
|
||
_clearSelfMicReleaseGuard();
|
||
}
|
||
_clearUserFromSeats(currentUserId);
|
||
_agoraRoleIsBroadcaster = false;
|
||
_syncSelfMicRuntimeState();
|
||
notifyListeners();
|
||
|
||
await _switchAgoraToAudience();
|
||
if (refreshMicList) {
|
||
requestMicrophoneListRefresh(
|
||
notifyIfUnchanged: false,
|
||
minInterval: const Duration(milliseconds: 350),
|
||
);
|
||
}
|
||
}
|
||
|
||
Future<RtcEngine> _initAgoraRtcEngine() {
|
||
if (engine != null) {
|
||
return Future.value(engine!);
|
||
}
|
||
final existingTask = _rtcEngineInitTask;
|
||
if (existingTask != null) {
|
||
return existingTask;
|
||
}
|
||
final task = _createAgoraRtcEngine();
|
||
_rtcEngineInitTask = task;
|
||
void clearInitTask() {
|
||
if (identical(_rtcEngineInitTask, task)) {
|
||
_rtcEngineInitTask = null;
|
||
}
|
||
}
|
||
|
||
unawaited(
|
||
task.then<void>(
|
||
(_) => clearInitTask(),
|
||
onError: (_, __) => clearInitTask(),
|
||
),
|
||
);
|
||
return task;
|
||
}
|
||
|
||
Future<RtcEngine> _createAgoraRtcEngine() async {
|
||
final rtcEngine = createAgoraRtcEngine();
|
||
await rtcEngine.initialize(
|
||
RtcEngineContext(appId: SCGlobalConfig.agoraRtcAppid),
|
||
);
|
||
engine = rtcEngine;
|
||
_rtcEngineEventHandler = RtcEngineEventHandler(
|
||
onError: (ErrorCodeType err, String msg) {
|
||
debugPrint('rtc错误$err');
|
||
final error = 'Agora error: $err $msg';
|
||
if (!_agoraJoined) {
|
||
final completer = _joinAgoraCompleter;
|
||
if (completer != null && !completer.isCompleted) {
|
||
completer.completeError(StateError(error));
|
||
} else {
|
||
unawaited(_handleAgoraJoinFailed(error, exitRoom: true));
|
||
}
|
||
return;
|
||
}
|
||
if (_isFatalAgoraError(err)) {
|
||
unawaited(_handleAgoraDisconnected(error));
|
||
}
|
||
},
|
||
onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
|
||
debugPrint('rtc 自己加入 ${connection.channelId} ${connection.localUid}');
|
||
final joinedChannelId = connection.channelId ?? "";
|
||
if (!_isCurrentAgoraChannel(joinedChannelId)) {
|
||
return;
|
||
}
|
||
_agoraJoined = true;
|
||
_agoraRoleIsBroadcaster = false;
|
||
_agoraJoinedChannelId = joinedChannelId.trim();
|
||
_cancelAgoraDisconnectedCleanup();
|
||
_joinAgoraTimeoutTimer?.cancel();
|
||
_joinAgoraTimeoutTimer = null;
|
||
final completer = _joinAgoraCompleter;
|
||
if (completer != null && !completer.isCompleted) {
|
||
completer.complete();
|
||
}
|
||
_resyncHeartbeatFromAgoraState();
|
||
},
|
||
onLeaveChannel: (RtcConnection connection, RtcStats stats) {
|
||
if (_isCurrentAgoraChannel(connection.channelId)) {
|
||
final completer = _joinAgoraCompleter;
|
||
if (completer != null && !completer.isCompleted) {
|
||
completer.completeError(
|
||
StateError('Agora left channel before join success'),
|
||
);
|
||
}
|
||
_resetAgoraTracking();
|
||
_resyncHeartbeatFromAgoraState();
|
||
}
|
||
},
|
||
onClientRoleChanged: (
|
||
RtcConnection connection,
|
||
ClientRoleType oldRole,
|
||
ClientRoleType newRole,
|
||
ClientRoleOptions newRoleOptions,
|
||
) {
|
||
if (!_isCurrentAgoraChannel(connection.channelId)) {
|
||
return;
|
||
}
|
||
_agoraRoleIsBroadcaster =
|
||
newRole == ClientRoleType.clientRoleBroadcaster;
|
||
_resyncHeartbeatFromAgoraState();
|
||
},
|
||
onClientRoleChangeFailed: (
|
||
RtcConnection connection,
|
||
ClientRoleChangeFailedReason reason,
|
||
ClientRoleType currentRole,
|
||
) {
|
||
if (!_isCurrentAgoraChannel(connection.channelId)) {
|
||
return;
|
||
}
|
||
_agoraRoleIsBroadcaster =
|
||
currentRole == ClientRoleType.clientRoleBroadcaster;
|
||
_resyncHeartbeatFromAgoraState();
|
||
},
|
||
onConnectionStateChanged: (
|
||
RtcConnection connection,
|
||
ConnectionStateType state,
|
||
ConnectionChangedReasonType reason,
|
||
) {
|
||
if (!_isCurrentAgoraChannel(connection.channelId)) {
|
||
return;
|
||
}
|
||
if (state == ConnectionStateType.connectionStateConnected &&
|
||
reason ==
|
||
ConnectionChangedReasonType.connectionChangedRejoinSuccess) {
|
||
_agoraJoined = true;
|
||
_agoraJoinedChannelId = connection.channelId?.trim();
|
||
_cancelAgoraDisconnectedCleanup();
|
||
_resyncHeartbeatFromAgoraState();
|
||
return;
|
||
}
|
||
if (reason ==
|
||
ConnectionChangedReasonType.connectionChangedLeaveChannel) {
|
||
_resetAgoraTracking();
|
||
_resyncHeartbeatFromAgoraState();
|
||
return;
|
||
}
|
||
if (state == ConnectionStateType.connectionStateFailed ||
|
||
_isFatalConnectionReason(reason)) {
|
||
_agoraJoined = false;
|
||
_agoraRoleIsBroadcaster = false;
|
||
_resyncHeartbeatFromAgoraState();
|
||
if (_joinAgoraCompleterIsPending) {
|
||
_joinAgoraCompleter?.completeError(
|
||
StateError('Agora connection failed before join: $reason'),
|
||
);
|
||
} else {
|
||
unawaited(
|
||
_handleAgoraDisconnected('Agora connection failed: $reason'),
|
||
);
|
||
}
|
||
return;
|
||
}
|
||
if (state == ConnectionStateType.connectionStateDisconnected ||
|
||
state == ConnectionStateType.connectionStateReconnecting) {
|
||
_agoraJoined = false;
|
||
_agoraRoleIsBroadcaster = false;
|
||
_resyncHeartbeatFromAgoraState();
|
||
_scheduleAgoraDisconnectedCleanup(reason);
|
||
}
|
||
},
|
||
onAudioMixingStateChanged: (
|
||
AudioMixingStateType state,
|
||
AudioMixingReasonType reason,
|
||
) {
|
||
switch (state) {
|
||
case AudioMixingStateType.audioMixingStatePlaying:
|
||
isMusicPlaying = true;
|
||
break;
|
||
case AudioMixingStateType.audioMixingStateStopped:
|
||
case AudioMixingStateType.audioMixingStateFailed:
|
||
isMusicPlaying = false;
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
_roomMusicMixingStateListener?.call(state, reason);
|
||
},
|
||
onUserJoined: (connection, remoteUid, elapsed) {
|
||
debugPrint('rtc用户 $remoteUid 加入了频道');
|
||
},
|
||
// 监听远端用户离开
|
||
onUserOffline: (connection, remoteUid, reason) {
|
||
debugPrint('rtc用户 $remoteUid 离开了频道 (原因: $reason)');
|
||
},
|
||
onTokenPrivilegeWillExpire: (
|
||
RtcConnection connection,
|
||
String token,
|
||
) async {
|
||
var rtcToken = await SCAccountRepository().getRtcToken(
|
||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||
AccountStorage().getCurrentUser()?.userProfile?.id ?? "",
|
||
isPublisher: _agoraRoleIsBroadcaster,
|
||
);
|
||
engine?.renewToken(rtcToken.rtcToken ?? "");
|
||
},
|
||
onAudioVolumeIndication: initializeAudioVolumeIndicationCallback,
|
||
);
|
||
rtcEngine.registerEventHandler(_rtcEngineEventHandler!);
|
||
await _configureAgoraAudioEngine(rtcEngine);
|
||
return rtcEngine;
|
||
}
|
||
|
||
Future<void> _configureAgoraAudioEngine(RtcEngine rtcEngine) async {
|
||
if (_agoraAudioConfigured) {
|
||
return;
|
||
}
|
||
await rtcEngine.setAudioProfile(
|
||
profile: AudioProfileType.audioProfileSpeechStandard,
|
||
scenario: AudioScenarioType.audioScenarioGameStreaming,
|
||
);
|
||
await rtcEngine.enableAudioVolumeIndication(
|
||
interval: 500,
|
||
smooth: 3,
|
||
reportVad: true,
|
||
);
|
||
await rtcEngine.disableVideo();
|
||
_agoraAudioConfigured = true;
|
||
}
|
||
|
||
Future<void> releaseRtcEngineForAppTermination() async {
|
||
final rtcEngine = engine;
|
||
if (rtcEngine == null) {
|
||
return;
|
||
}
|
||
|
||
final eventHandler = _rtcEngineEventHandler;
|
||
engine = null;
|
||
_rtcEngineEventHandler = null;
|
||
_rtcEngineInitTask = null;
|
||
_rtcEnginePrewarmTask = null;
|
||
_agoraAudioConfigured = false;
|
||
|
||
try {
|
||
if (eventHandler != null) {
|
||
rtcEngine.unregisterEventHandler(eventHandler);
|
||
}
|
||
await rtcEngine.leaveChannel();
|
||
} catch (e) {
|
||
debugPrint('rtc销毁前离开频道出错: $e');
|
||
}
|
||
_resetAgoraTracking();
|
||
|
||
try {
|
||
await rtcEngine.release();
|
||
} catch (e) {
|
||
debugPrint('rtc释放引擎出错: $e');
|
||
}
|
||
}
|
||
|
||
void initializeAudioVolumeIndicationCallback(
|
||
RtcConnection connection,
|
||
List<AudioVolumeInfo> speakers,
|
||
int speakerNumber,
|
||
int totalVolume,
|
||
) {
|
||
// if (user == null) {
|
||
// throw "本地User尚未初始化";
|
||
// }
|
||
// print('初始化声音监听器:${user.toJson()}');
|
||
|
||
if (roomWheatMap.isEmpty || speakers.isEmpty) {
|
||
return;
|
||
}
|
||
|
||
for (final seat in roomWheatMap.values) {
|
||
seat.setVolume = 0;
|
||
}
|
||
|
||
final seatIndexByAgoraUid = <int, num>{};
|
||
roomWheatMap.forEach((seatIndex, mic) {
|
||
final user = mic.user;
|
||
if (user == null) {
|
||
return;
|
||
}
|
||
seatIndexByAgoraUid[_resolveAgoraUidForUser(user)] = seatIndex;
|
||
});
|
||
|
||
final currentUserAgoraUid = _resolveAgoraUidForCurrentUser();
|
||
final listeners = List<OnSoundVoiceChange>.from(_onSoundVoiceChangeList);
|
||
for (final info in speakers) {
|
||
final volume = info.volume ?? 0;
|
||
if (volume <= 0) {
|
||
continue;
|
||
}
|
||
|
||
final resolvedAgoraUid = info.uid == 0 ? currentUserAgoraUid : info.uid;
|
||
final seatIndex = seatIndexByAgoraUid[resolvedAgoraUid];
|
||
if (seatIndex == null) {
|
||
continue;
|
||
}
|
||
if (!shouldShowSeatSoundWave(seatIndex)) {
|
||
continue;
|
||
}
|
||
|
||
roomWheatMap[seatIndex]?.setVolume = volume;
|
||
for (final listener in listeners) {
|
||
listener(seatIndex, volume);
|
||
}
|
||
}
|
||
}
|
||
|
||
///关注/取消房间
|
||
void followCurrentVoiceRoom() async {
|
||
var result = await SCAccountRepository().followRoom(
|
||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||
);
|
||
if (isFollowRoomRes != null) {
|
||
isFollowRoomRes = isFollowRoomRes!.copyWith(
|
||
followRoom: !isFollowRoomRes!.followRoom!,
|
||
);
|
||
} else {
|
||
isFollowRoomRes = SCIsFollowRoomRes(followRoom: result);
|
||
}
|
||
SmartDialog.dismiss(tag: "unFollowDialog");
|
||
notifyListeners();
|
||
}
|
||
|
||
int startTime = 0;
|
||
|
||
String? _preferNonEmpty(String? primary, String? fallback) {
|
||
if ((primary ?? "").trim().isNotEmpty) {
|
||
return primary;
|
||
}
|
||
if ((fallback ?? "").trim().isNotEmpty) {
|
||
return fallback;
|
||
}
|
||
return primary ?? fallback;
|
||
}
|
||
|
||
void updateCurrentRoomBasicInfo({
|
||
String? roomCover,
|
||
String? roomBackground,
|
||
String? roomName,
|
||
String? roomDesc,
|
||
}) {
|
||
final currentRoomProfile = currenRoom?.roomProfile?.roomProfile;
|
||
if (currenRoom == null || currentRoomProfile == null) {
|
||
return;
|
||
}
|
||
currenRoom = currenRoom?.copyWith(
|
||
roomProfile: currenRoom?.roomProfile?.copyWith(
|
||
roomProfile: currentRoomProfile.copyWith(
|
||
roomCover: _preferNonEmpty(roomCover, currentRoomProfile.roomCover),
|
||
roomBackground: _preferNonEmpty(
|
||
roomBackground,
|
||
currentRoomProfile.roomBackground,
|
||
),
|
||
roomName: _preferNonEmpty(roomName, currentRoomProfile.roomName),
|
||
roomDesc: _preferNonEmpty(roomDesc, currentRoomProfile.roomDesc),
|
||
),
|
||
),
|
||
);
|
||
SCRoomProfileCache.saveRoomProfile(
|
||
roomId: currentRoomProfile.id ?? "",
|
||
roomCover: roomCover,
|
||
roomBackground: roomBackground,
|
||
roomName: roomName,
|
||
roomDesc: roomDesc,
|
||
);
|
||
notifyListeners();
|
||
}
|
||
|
||
void joinVoiceRoomSession(
|
||
BuildContext context,
|
||
String roomId, {
|
||
String? pwd,
|
||
bool clearRoomData = false,
|
||
bool needOpenRedenvelope = false,
|
||
String redPackId = "",
|
||
int? previewSeatCount,
|
||
RoomEntryPreviewData? previewData,
|
||
}) async {
|
||
int nextTime = DateTime.now().millisecondsSinceEpoch;
|
||
if (nextTime - startTime < 1000) {
|
||
//频繁点击
|
||
return;
|
||
}
|
||
startTime = nextTime;
|
||
if (clearRoomData) {
|
||
_clearData();
|
||
}
|
||
bool hasPermission = await SCPermissionUtils.checkMicrophonePermission();
|
||
if (!hasPermission) {
|
||
SCTts.show('Microphone permission is denied.');
|
||
throw ArgumentError('Microphone permission is denied.');
|
||
}
|
||
if (roomId == currenRoom?.roomProfile?.roomProfile?.id) {
|
||
if (_currentRoomIsEntryPreview ||
|
||
_roomStartupStatus == RoomStartupStatus.loading) {
|
||
SCFloatIchart().remove();
|
||
if (!context.mounted) {
|
||
return;
|
||
}
|
||
VoiceRoomRoute.openVoiceRoom(context);
|
||
return;
|
||
}
|
||
|
||
///最小化进入房间,或者进入的是同一个房间
|
||
final loaded = await loadRoomInfo(
|
||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||
);
|
||
if (!loaded) {
|
||
return;
|
||
}
|
||
if (!context.mounted) {
|
||
return;
|
||
}
|
||
Provider.of<SocialChatUserProfileManager>(
|
||
context,
|
||
listen: false,
|
||
).fetchUserProfileData();
|
||
retrieveMicrophoneList();
|
||
fetchOnlineUsersList();
|
||
_startRoomStatePolling();
|
||
_setRoomStartupReady();
|
||
setRoomVisualEffectsEnabled(true);
|
||
SCFloatIchart().remove();
|
||
VoiceRoomRoute.openVoiceRoom(context);
|
||
} else {
|
||
SCFloatIchart().remove();
|
||
if (currenRoom != null) {
|
||
_switchAwayFromCurrentRoomForNewEntry();
|
||
}
|
||
if (!context.mounted) {
|
||
return;
|
||
}
|
||
rtmProvider = Provider.of<RtmProvider>(context, listen: false);
|
||
_clearRoomContributionLevelData(context);
|
||
rtmProvider?.seedSystemRoomTips(
|
||
SCAppLocalizations.of(context)!.systemRoomTips,
|
||
);
|
||
_setRoomStartupLoading();
|
||
setRoomVisualEffectsEnabled(true);
|
||
_applyRoomEntryPreview(
|
||
roomId,
|
||
previewData: previewData,
|
||
previewSeatCount: previewSeatCount,
|
||
);
|
||
VoiceRoomRoute.openVoiceRoom(context);
|
||
notifyListeners();
|
||
final entryRequestSerial = ++_roomEntryRequestSerial;
|
||
unawaited(
|
||
_enterAndInitializeVoiceRoomSession(
|
||
roomId,
|
||
entryRequestSerial: entryRequestSerial,
|
||
pwd: pwd,
|
||
needOpenRedenvelope: needOpenRedenvelope,
|
||
redPackId: redPackId,
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
Future<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,
|
||
);
|
||
if (!_isActiveRoomEntryRequest(entryRequestSerial)) {
|
||
unawaited(_cleanupStaleEnteredRoom(enteredRoom));
|
||
return;
|
||
}
|
||
currenRoom = _mergeVoiceRoomVipHints(enteredRoom, currenRoom);
|
||
_syncCurrentUserVipHintFromRoom(currenRoom);
|
||
unawaited(_refreshCurrentUserVipHintForRoom(entryRequestSerial));
|
||
_currentRoomIsEntryPreview = false;
|
||
_previewRoomSeatCount = null;
|
||
notifyListeners();
|
||
await initializeRoomSession(
|
||
entryRequestSerial: entryRequestSerial,
|
||
needOpenRedenvelope: needOpenRedenvelope,
|
||
redPackId: redPackId,
|
||
shouldOpenRoomPage: false,
|
||
);
|
||
} catch (e, stackTrace) {
|
||
if (!_isActiveRoomEntryRequest(entryRequestSerial)) {
|
||
return;
|
||
}
|
||
debugPrint('[RoomStartup] entry room failed: $e\n$stackTrace');
|
||
_setRoomStartupFailed(RoomStartupFailureType.entry);
|
||
}
|
||
}
|
||
|
||
JoinRoomRes _mergeVoiceRoomVipHints(
|
||
JoinRoomRes enteredRoom,
|
||
JoinRoomRes? previewRoom,
|
||
) {
|
||
final roomProfile = enteredRoom.roomProfile;
|
||
if (roomProfile == null) {
|
||
return enteredRoom;
|
||
}
|
||
|
||
final enteredOwner = roomProfile.userProfile;
|
||
if (_profileHasPositiveVipLevelHint(enteredOwner)) {
|
||
return enteredRoom;
|
||
}
|
||
|
||
final roomId = _firstNonBlankText([roomProfile.roomProfile?.id]);
|
||
final ownerId = _firstNonBlankText([
|
||
roomProfile.roomProfile?.userId,
|
||
enteredOwner?.id,
|
||
]);
|
||
final previewRoomProfile = previewRoom?.roomProfile;
|
||
final previewOwner = previewRoomProfile?.userProfile;
|
||
final previewRoomId = _firstNonBlankText([
|
||
previewRoomProfile?.roomProfile?.id,
|
||
]);
|
||
final previewOwnerId = _firstNonBlankText([
|
||
previewRoomProfile?.roomProfile?.userId,
|
||
previewOwner?.id,
|
||
]);
|
||
final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
|
||
|
||
SocialChatUserProfile? resolvedOwner = enteredOwner;
|
||
String? vipLevel;
|
||
|
||
final canUsePreviewOwner =
|
||
previewOwner != null &&
|
||
_sameNonBlankId(roomId, previewRoomId) &&
|
||
(ownerId == null || _sameNonBlankId(ownerId, previewOwnerId));
|
||
if (canUsePreviewOwner) {
|
||
vipLevel = _profileVipLevelHint(previewOwner);
|
||
resolvedOwner ??= previewOwner;
|
||
}
|
||
|
||
if (vipLevel == null && _sameNonBlankId(ownerId, currentProfile?.id)) {
|
||
vipLevel =
|
||
_profileVipLevelHint(currentProfile) ??
|
||
_roomVipLevelHintFromEntrants(enteredRoom.entrants);
|
||
resolvedOwner ??= currentProfile;
|
||
}
|
||
|
||
if (vipLevel == null || resolvedOwner == null) {
|
||
return enteredRoom;
|
||
}
|
||
|
||
return enteredRoom.copyWith(
|
||
roomProfile: roomProfile.copyWith(
|
||
userProfile: resolvedOwner.copyWith(vipLevel: vipLevel),
|
||
userSVipLevel: vipLevel,
|
||
),
|
||
);
|
||
}
|
||
|
||
void _syncCurrentUserVipHintFromRoom(JoinRoomRes? room) {
|
||
final currentUser = AccountStorage().getCurrentUser();
|
||
final currentProfile = currentUser?.userProfile;
|
||
if (currentUser == null ||
|
||
currentProfile == null ||
|
||
_profileHasPositiveVipLevelHint(currentProfile)) {
|
||
return;
|
||
}
|
||
|
||
final ownerProfile = room?.roomProfile?.userProfile;
|
||
final ownerId = _firstNonBlankText([
|
||
room?.roomProfile?.roomProfile?.userId,
|
||
ownerProfile?.id,
|
||
]);
|
||
String? vipLevel;
|
||
if (_sameNonBlankId(ownerId, currentProfile.id)) {
|
||
vipLevel = _profileVipLevelHint(ownerProfile);
|
||
}
|
||
vipLevel ??= _roomVipLevelHintFromEntrants(room?.entrants);
|
||
if (vipLevel == null) {
|
||
return;
|
||
}
|
||
|
||
AccountStorage().setCurrentUser(
|
||
currentUser.copyWith(
|
||
userProfile: currentProfile.copyWith(vipLevel: vipLevel),
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<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 vipLevel =
|
||
existingVipLevel ?? await _loadCurrentVipLevelForColoredId();
|
||
if (vipLevel == null || vipLevel.isEmpty) {
|
||
return;
|
||
}
|
||
|
||
final updatedProfile = currentProfile.copyWith(vipLevel: vipLevel);
|
||
if (!_profileHasPositiveVipLevelHint(currentProfile)) {
|
||
AccountStorage().setCurrentUser(
|
||
currentUser.copyWith(userProfile: updatedProfile),
|
||
);
|
||
}
|
||
|
||
if (!_isCurrentRoomEntrySession(entryRequestSerial, roomId)) {
|
||
return;
|
||
}
|
||
|
||
var changed = false;
|
||
final mergedRoom = _mergeKnownVipIntoCurrentRoom(
|
||
currenRoom,
|
||
updatedProfile,
|
||
);
|
||
if (!identical(mergedRoom, currenRoom)) {
|
||
currenRoom = mergedRoom;
|
||
changed = true;
|
||
}
|
||
|
||
final mergedOnlineUsers =
|
||
onlineUsers
|
||
.map((user) => _mergeKnownVipIntoProfile(user) ?? user)
|
||
.toList();
|
||
if (!_sameOnlineUsers(onlineUsers, mergedOnlineUsers)) {
|
||
onlineUsers = mergedOnlineUsers;
|
||
_refreshManagerUsers(onlineUsers);
|
||
changed = true;
|
||
}
|
||
|
||
final mergedMicMap = roomWheatMap.map(
|
||
(index, mic) => MapEntry(index, _mergeKnownVipIntoMic(mic)),
|
||
);
|
||
if (!_sameMicMaps(roomWheatMap, mergedMicMap)) {
|
||
roomWheatMap = mergedMicMap;
|
||
changed = true;
|
||
}
|
||
|
||
if (changed) {
|
||
notifyListeners();
|
||
}
|
||
}
|
||
|
||
Future<String?> _loadCurrentVipLevelForColoredId() async {
|
||
final repository = SCVipRepositoryImp();
|
||
try {
|
||
final home = await repository.vipHome();
|
||
final homeVipLevel = _vipLevelFromStatus(home.state);
|
||
if (homeVipLevel != null) {
|
||
return homeVipLevel;
|
||
}
|
||
} catch (_) {}
|
||
|
||
try {
|
||
return _vipLevelFromStatus(await repository.vipStatus());
|
||
} catch (_) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
String? _vipLevelFromStatus(SCVipStatusRes? status) {
|
||
if (status == null) {
|
||
return null;
|
||
}
|
||
if (status.active == false) {
|
||
return '0';
|
||
}
|
||
final level = status.levelInt;
|
||
return level > 0 ? level.toString() : '0';
|
||
}
|
||
|
||
JoinRoomRes? _mergeKnownVipIntoCurrentRoom(
|
||
JoinRoomRes? room,
|
||
SocialChatUserProfile currentProfile,
|
||
) {
|
||
final roomProfile = room?.roomProfile;
|
||
final ownerProfile = roomProfile?.userProfile;
|
||
if (room == null || roomProfile == null || ownerProfile == null) {
|
||
return room;
|
||
}
|
||
|
||
final ownerId = _firstNonBlankText([
|
||
roomProfile.roomProfile?.userId,
|
||
ownerProfile.id,
|
||
]);
|
||
if (!_sameNonBlankId(ownerId, currentProfile.id) ||
|
||
_profileHasPositiveVipLevelHint(ownerProfile)) {
|
||
return room;
|
||
}
|
||
|
||
final vipLevel = _positiveProfileVipLevelHint(currentProfile);
|
||
if (vipLevel == null) {
|
||
return room;
|
||
}
|
||
|
||
return room.copyWith(
|
||
roomProfile: roomProfile.copyWith(
|
||
userProfile: ownerProfile.copyWith(vipLevel: vipLevel),
|
||
userSVipLevel: vipLevel,
|
||
),
|
||
);
|
||
}
|
||
|
||
MicRes _mergeKnownVipIntoMic(MicRes mic) {
|
||
final mergedUser = _mergeKnownVipIntoProfile(mic.user);
|
||
if (identical(mergedUser, mic.user)) {
|
||
return mic;
|
||
}
|
||
return mic.copyWith(user: mergedUser);
|
||
}
|
||
|
||
SocialChatUserProfile? _mergeKnownVipIntoProfile(
|
||
SocialChatUserProfile? profile,
|
||
) {
|
||
if (profile == null || _profileHasPositiveVipLevelHint(profile)) {
|
||
return profile;
|
||
}
|
||
|
||
final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
|
||
final currentVipLevel = _positiveProfileVipLevelHint(currentProfile);
|
||
if (_sameNonBlankId(profile.id, currentProfile?.id) &&
|
||
currentVipLevel != null) {
|
||
return profile.copyWith(vipLevel: currentVipLevel);
|
||
}
|
||
|
||
final ownerProfile = currenRoom?.roomProfile?.userProfile;
|
||
final ownerVipLevel = _positiveProfileVipLevelHint(ownerProfile);
|
||
final ownerId = _firstNonBlankText([
|
||
currenRoom?.roomProfile?.roomProfile?.userId,
|
||
ownerProfile?.id,
|
||
]);
|
||
if (_sameNonBlankId(profile.id, ownerId) && ownerVipLevel != null) {
|
||
return profile.copyWith(vipLevel: ownerVipLevel);
|
||
}
|
||
|
||
return profile;
|
||
}
|
||
|
||
bool _profileHasPositiveVipLevelHint(SocialChatUserProfile? profile) {
|
||
return _positiveProfileVipLevelHint(profile) != null;
|
||
}
|
||
|
||
String? _profileVipLevelHint(SocialChatUserProfile? profile) {
|
||
return _firstNonBlankText([profile?.vipLevel]);
|
||
}
|
||
|
||
String? _positiveProfileVipLevelHint(SocialChatUserProfile? profile) {
|
||
final vipLevel = _profileVipLevelHint(profile);
|
||
if (profile == null || profile.vipLevelForColoredId <= 0) {
|
||
return null;
|
||
}
|
||
return vipLevel ?? profile.vipLevelForColoredId.toString();
|
||
}
|
||
|
||
String? _roomVipLevelHintFromEntrants(Entrants? entrants) {
|
||
final abilities = entrants?.nobleVipAbility ?? const <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)) {
|
||
debugPrint(
|
||
'[RoomStartup] skip stale entry cleanup: room $staleRoomId is active again',
|
||
);
|
||
return;
|
||
}
|
||
await _retryExitNetworkRequest('quit stale entered room $staleRoomId', () {
|
||
return SCAccountRepository().quitRoom(staleRoomId).then((didQuit) {
|
||
if (!didQuit) {
|
||
throw StateError('quitRoom returned false');
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
///初始化房间相关
|
||
Future<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!);
|
||
}
|
||
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 rtcTokenFuture = _captureStartupFuture(
|
||
_fetchRtcTokenForCurrentRoom(isPublisher: false),
|
||
);
|
||
final microphoneListFuture = retrieveMicrophoneList();
|
||
final onlineUsersFuture = fetchOnlineUsersList();
|
||
final joinResult = await joinGroupFuture;
|
||
if (!_isActiveRoomStartup(entryRequestSerial, roomId)) {
|
||
return;
|
||
}
|
||
if (joinResult == null || joinResult.code != 0) {
|
||
debugPrint(
|
||
'[RoomStartup] join IM group failed code=${joinResult?.code} desc=${joinResult?.desc}',
|
||
);
|
||
_setRoomStartupFailed(RoomStartupFailureType.im);
|
||
return;
|
||
}
|
||
|
||
rtmProvider?.seedSystemRoomTips(
|
||
SCAppLocalizations.of(context!)!.systemRoomTips,
|
||
groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "",
|
||
);
|
||
rtmProvider?.addMsg(
|
||
Msg(
|
||
groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "",
|
||
msg: currenRoom?.roomProfile?.roomProfile?.roomDesc,
|
||
type: SCRoomMsgType.systemTips,
|
||
),
|
||
);
|
||
|
||
await Future.wait([microphoneListFuture, onlineUsersFuture]);
|
||
if (!_isActiveRoomStartup(entryRequestSerial, roomId)) {
|
||
return;
|
||
}
|
||
_startRoomStatePolling();
|
||
final currentContext = context;
|
||
if (currentContext != null && currentContext.mounted) {
|
||
Provider.of<SocialChatRoomManager>(
|
||
currentContext,
|
||
listen: false,
|
||
).fetchContributionLevelData(roomId);
|
||
}
|
||
|
||
failureType = RoomStartupFailureType.rtc;
|
||
final rtcTokenResult = await rtcTokenFuture;
|
||
if (!_isActiveRoomStartup(entryRequestSerial, roomId)) {
|
||
return;
|
||
}
|
||
final rtcToken = rtcTokenResult.requireValue();
|
||
await joinAgoraVoiceChannel(throwOnError: true, rtcToken: rtcToken);
|
||
if (!_isActiveRoomStartup(entryRequestSerial, roomId)) {
|
||
await _cleanupAgoraRoomState(
|
||
clearMicSeats: currenRoom == null,
|
||
onlyIfJoinedChannelId: roomId,
|
||
);
|
||
return;
|
||
}
|
||
await _bootstrapRoomHeartbeatState();
|
||
if (!_isActiveRoomStartup(entryRequestSerial, roomId)) {
|
||
return;
|
||
}
|
||
_setRoomStartupReady();
|
||
|
||
Msg joinMsg = Msg(
|
||
groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "",
|
||
user: AccountStorage().getCurrentUser()?.userProfile,
|
||
msg: "",
|
||
role: currenRoom?.entrants?.roles ?? "",
|
||
type: SCRoomMsgType.joinRoom,
|
||
);
|
||
rtmProvider?.dispatchMessage(joinMsg, addLocal: true);
|
||
_playRoomEntryEffects(
|
||
joinMsg,
|
||
entryRequestSerial: entryRequestSerial,
|
||
roomId: roomId,
|
||
);
|
||
_loadRoomSecondaryData();
|
||
} catch (error, stackTrace) {
|
||
if (!_isActiveRoomStartup(entryRequestSerial, roomId)) {
|
||
debugPrint('[RoomStartup] ignore canceled bootstrap failure: $error');
|
||
return;
|
||
}
|
||
debugPrint('[RoomStartup] bootstrap failed: $error\n$stackTrace');
|
||
_setRoomStartupFailed(failureType);
|
||
}
|
||
}
|
||
|
||
void _playRoomEntryEffects(
|
||
Msg joinMsg, {
|
||
required int entryRequestSerial,
|
||
required String roomId,
|
||
}) {
|
||
if (SCGlobalConfig.isEntryVehicleAnimation) {
|
||
final sourceUrl =
|
||
AccountStorage()
|
||
.getCurrentUser()
|
||
?.userProfile
|
||
?.getMountains()
|
||
?.sourceUrl ??
|
||
"";
|
||
if (sourceUrl.isNotEmpty &&
|
||
shouldShowRoomVisualEffects &&
|
||
_isCurrentRoomEntrySession(entryRequestSerial, roomId)) {
|
||
_roomEntryEffectTimer?.cancel();
|
||
_roomEntryEffectTimer = Timer(const Duration(milliseconds: 550), () {
|
||
_roomEntryEffectTimer = null;
|
||
if (!shouldShowRoomVisualEffects ||
|
||
!_isCurrentRoomEntrySession(entryRequestSerial, roomId)) {
|
||
return;
|
||
}
|
||
SCGiftVapSvgaManager().play(
|
||
sourceUrl,
|
||
priority: SCGiftVapSvgaManager.entryEffectPriority,
|
||
type: SCGiftVapSvgaManager.entryEffectType,
|
||
);
|
||
});
|
||
}
|
||
}
|
||
if (rtmProvider?.msgUserJoinListener != null) {
|
||
rtmProvider?.msgUserJoinListener!(joinMsg);
|
||
}
|
||
}
|
||
|
||
void _loadRoomSecondaryData() {
|
||
SCChatRoomRepository()
|
||
.rocketClaim(currenRoom?.roomProfile?.roomProfile?.id ?? "")
|
||
.catchError((e) {
|
||
return true; // 错误已处理
|
||
});
|
||
SCChatRoomRepository()
|
||
.rocketStatus(currenRoom?.roomProfile?.roomProfile?.id ?? "")
|
||
.then((res) {
|
||
roomRocketStatus = res;
|
||
notifyListeners();
|
||
})
|
||
.catchError((e) {});
|
||
loadRoomRedPacketList(1);
|
||
|
||
fetchRoomTaskClaimableCount();
|
||
}
|
||
|
||
///更新房间火箭信息
|
||
void updateRoomRocketConfigurationStatus(SCRoomRocketStatusRes res) {
|
||
roomRocketStatus = res;
|
||
notifyListeners();
|
||
}
|
||
|
||
///获取在线用户
|
||
Future<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;
|
||
debugPrint(
|
||
'[Room][OnlineUsers] suspend auto refresh for current session: ${_errorMessageFrom(error)}',
|
||
);
|
||
} else {
|
||
debugPrint(
|
||
'[Room][OnlineUsers] refresh failed: ${_errorMessageFrom(error)}',
|
||
);
|
||
}
|
||
} 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;
|
||
}
|
||
final nextMap = _buildMicMap(roomWheatList);
|
||
_applyMicSnapshot(nextMap, notifyIfUnchanged: notifyIfUnchanged);
|
||
} catch (error) {
|
||
if (_isMissingPathError(error)) {
|
||
_disableMicListRefreshForCurrentSession = true;
|
||
_deferredMicListRefreshTimer?.cancel();
|
||
_deferredMicListRefreshTimer = null;
|
||
debugPrint(
|
||
'[Room][MicList] suspend auto refresh for current session: ${_errorMessageFrom(error)}',
|
||
);
|
||
if (notifyIfUnchanged) {
|
||
notifyListeners();
|
||
}
|
||
} else {
|
||
debugPrint(
|
||
'[Room][MicList] refresh failed: ${_errorMessageFrom(error)}',
|
||
);
|
||
}
|
||
} finally {
|
||
_isRefreshingMicList = false;
|
||
if (_pendingMicListRefresh) {
|
||
_pendingMicListRefresh = false;
|
||
final pendingNotifyIfUnchanged =
|
||
_pendingMicListRefreshNotifyIfUnchanged;
|
||
_pendingMicListRefreshNotifyIfUnchanged = false;
|
||
unawaited(
|
||
Future<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 {
|
||
var result = await SCChatRoomRepository().roomRedPacketList(
|
||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||
current,
|
||
);
|
||
if (current == 1) {
|
||
redPacketList = result;
|
||
notifyListeners();
|
||
}
|
||
return result;
|
||
}
|
||
|
||
Future exitCurrentVoiceRoomSession(
|
||
bool isLogout, {
|
||
bool deferRemoteCleanup = true,
|
||
}) async {
|
||
if (_isExitingCurrentVoiceRoomSession) {
|
||
return;
|
||
}
|
||
_setExitingCurrentVoiceRoomSession(true);
|
||
_stopRoomStatePolling();
|
||
final groupId = currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "";
|
||
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||
final isPreviewOnlyExit = _currentRoomIsEntryPreview;
|
||
final exitRtmProvider = rtmProvider;
|
||
final navigationContext = context;
|
||
try {
|
||
exitRtmProvider?.msgAllListener = null;
|
||
exitRtmProvider?.msgChatListener = null;
|
||
exitRtmProvider?.msgGiftListener = null;
|
||
SCGiftVapSvgaManager().stopPlayback();
|
||
setRoomVisualEffectsEnabled(false);
|
||
SCHeartbeatUtils.cancelTimer();
|
||
exitRtmProvider?.cleanRoomData();
|
||
} catch (e) {
|
||
debugPrint('rtc退出房间本地清理出错: $e');
|
||
}
|
||
|
||
_clearData();
|
||
if (!isLogout && navigationContext != null) {
|
||
SCNavigatorUtils.popUntil(
|
||
navigationContext,
|
||
ModalRoute.withName(SCRoutes.home),
|
||
);
|
||
}
|
||
|
||
final agoraLeaveTask = _leaveAgoraRoomForExit();
|
||
final remoteCleanupTask = _cleanupExitedRoomRemotely(
|
||
exitRtmProvider: exitRtmProvider,
|
||
groupId: isPreviewOnlyExit ? "" : groupId,
|
||
roomId: isPreviewOnlyExit ? "" : roomId,
|
||
shouldSendOnlineHeartbeat: !isLogout,
|
||
);
|
||
if (deferRemoteCleanup) {
|
||
unawaited(agoraLeaveTask);
|
||
unawaited(remoteCleanupTask);
|
||
} else {
|
||
await agoraLeaveTask;
|
||
await remoteCleanupTask;
|
||
}
|
||
}
|
||
|
||
void _switchAwayFromCurrentRoomForNewEntry() {
|
||
_stopRoomStatePolling();
|
||
final groupId = currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "";
|
||
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||
final isPreviewOnlyExit = _currentRoomIsEntryPreview;
|
||
final exitRtmProvider = rtmProvider;
|
||
try {
|
||
exitRtmProvider?.msgAllListener = null;
|
||
exitRtmProvider?.msgChatListener = null;
|
||
exitRtmProvider?.msgGiftListener = null;
|
||
SCGiftVapSvgaManager().stopPlayback();
|
||
setRoomVisualEffectsEnabled(false);
|
||
SCHeartbeatUtils.cancelTimer();
|
||
exitRtmProvider?.cleanRoomData();
|
||
} catch (e) {
|
||
debugPrint('rtc切换房间本地清理出错: $e');
|
||
}
|
||
|
||
_clearData();
|
||
final agoraLeaveTask = _leaveAgoraRoomForExit();
|
||
_pendingRoomSwitchAgoraLeaveTask = agoraLeaveTask;
|
||
unawaited(
|
||
agoraLeaveTask.whenComplete(() {
|
||
if (identical(_pendingRoomSwitchAgoraLeaveTask, agoraLeaveTask)) {
|
||
_pendingRoomSwitchAgoraLeaveTask = null;
|
||
}
|
||
}),
|
||
);
|
||
unawaited(
|
||
_cleanupExitedRoomRemotely(
|
||
exitRtmProvider: exitRtmProvider,
|
||
groupId: isPreviewOnlyExit ? "" : groupId,
|
||
roomId: isPreviewOnlyExit ? "" : roomId,
|
||
shouldSendOnlineHeartbeat: false,
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<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) {
|
||
debugPrint('[RoomExit] skip ONLINE heartbeat: already in a room');
|
||
return;
|
||
}
|
||
await SCHeartbeatUtils.scheduleHeartbeat(
|
||
SCHeartbeatStatus.ONLINE.name,
|
||
false,
|
||
rethrowOnError: true,
|
||
);
|
||
}),
|
||
);
|
||
}
|
||
if (groupId.isNotEmpty && exitRtmProvider != null) {
|
||
tasks.add(
|
||
_retryExitNetworkRequest('quit IM group $groupId', () async {
|
||
if (_isCurrentRoomGroup(groupId)) {
|
||
debugPrint('[RoomExit] skip quitGroup: re-entered group $groupId');
|
||
return;
|
||
}
|
||
await exitRtmProvider.quitGroup(groupId);
|
||
}),
|
||
);
|
||
}
|
||
if (roomId.isNotEmpty) {
|
||
tasks.add(
|
||
_retryExitNetworkRequest('quit room $roomId', () async {
|
||
if (_isCurrentRoomId(roomId)) {
|
||
debugPrint('[RoomExit] skip quitRoom: re-entered room $roomId');
|
||
return;
|
||
}
|
||
final didQuit = await SCAccountRepository().quitRoom(roomId);
|
||
if (!didQuit) {
|
||
throw StateError('quitRoom returned false');
|
||
}
|
||
}),
|
||
);
|
||
}
|
||
await Future.wait(tasks);
|
||
}
|
||
|
||
Future<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;
|
||
debugPrint(
|
||
'[RoomExit] $name failed attempt '
|
||
'$attempt/$_exitNetworkRetryLimit: $error',
|
||
);
|
||
if (attempt < _exitNetworkRetryLimit) {
|
||
await Future.delayed(_exitNetworkRetryDelay * attempt);
|
||
}
|
||
}
|
||
}
|
||
debugPrint(
|
||
'[RoomExit] $name failed after $_exitNetworkRetryLimit attempts, '
|
||
'treat as backend/remote issue: $lastError\n$lastStackTrace',
|
||
);
|
||
}
|
||
|
||
bool _isCurrentRoomId(String roomId) {
|
||
return roomId.isNotEmpty &&
|
||
currenRoom?.roomProfile?.roomProfile?.id == roomId;
|
||
}
|
||
|
||
bool _isCurrentRoomGroup(String groupId) {
|
||
return groupId.isNotEmpty &&
|
||
currenRoom?.roomProfile?.roomProfile?.roomAccount == groupId;
|
||
}
|
||
|
||
Future<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 _cleanupAgoraRoomState();
|
||
await Future.delayed(const Duration(milliseconds: 100));
|
||
} catch (e) {
|
||
debugPrint('rtc清理本地房间状态出错: $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();
|
||
rtmProvider?.cleanRoomData();
|
||
if (!isPreviewOnlyFailure && groupId.isNotEmpty) {
|
||
await rtmProvider?.quitGroup(groupId);
|
||
}
|
||
await _cleanupAgoraRoomState(clearMicSeats: true);
|
||
await Future.delayed(const Duration(milliseconds: 100));
|
||
if (!isPreviewOnlyFailure && roomId.isNotEmpty) {
|
||
await SCAccountRepository().quitRoom(roomId);
|
||
}
|
||
} catch (e) {
|
||
debugPrint('rtc进房失败清理本地房间状态出错: $e');
|
||
} finally {
|
||
_clearData();
|
||
_isHandlingRoomStartupFailure = false;
|
||
notifyListeners();
|
||
if (navigator != null && navigator.mounted && navigator.canPop()) {
|
||
navigator.pop();
|
||
}
|
||
}
|
||
}
|
||
|
||
///清空列表数据
|
||
void _clearData() {
|
||
_roomEntryRequestSerial += 1;
|
||
_roomEntryEffectTimer?.cancel();
|
||
_roomEntryEffectTimer = null;
|
||
_previewRoomSeatCount = null;
|
||
_finishRoomStartupSeatLoading(executePendingAction: false);
|
||
_resetSelfMicGoUpFailureState();
|
||
_clearRoomContributionLevelData();
|
||
_stopRoomStatePolling();
|
||
_resetHeartbeatTracking();
|
||
_resetAgoraTracking();
|
||
_disableMicListRefreshForCurrentSession = false;
|
||
_disableOnlineUsersRefreshForCurrentSession = false;
|
||
_isSelfMicActionInFlight = false;
|
||
_roomStartupStatus = RoomStartupStatus.idle;
|
||
_roomStartupFailureType = RoomStartupFailureType.none;
|
||
_currentRoomIsEntryPreview = false;
|
||
_isHandlingRoomStartupFailure = false;
|
||
_isHandlingAgoraRoomFailure = false;
|
||
_pendingMicListRefresh = false;
|
||
_pendingMicListRefreshNotifyIfUnchanged = false;
|
||
_seatEmojiEventVersions.clear();
|
||
_emojiPlaybackEventVersion = 0;
|
||
roomRocketStatus = null;
|
||
rtmProvider
|
||
?.onNewMessageListenerGroupMap["${currenRoom?.roomProfile?.roomProfile?.roomAccount}"] =
|
||
null;
|
||
roomWheatMap.clear();
|
||
onlineUsers.clear();
|
||
managerUsers.clear();
|
||
needUpDataUserInfo = false;
|
||
SCRoomUtils.roomUsersMap.clear();
|
||
_clearSelfMicSwitchGuard(clearPreferredIndex: true);
|
||
_clearSelfMicReleaseGuard();
|
||
roomIsMute = false;
|
||
rtmProvider?.roomAllMsgList.clear();
|
||
rtmProvider?.roomChatMsgList.clear();
|
||
redPacketList.clear();
|
||
currenRoom = null;
|
||
_roomVisualEffectsEnabled = false;
|
||
_isExitingCurrentVoiceRoomSession = false;
|
||
isMic = true;
|
||
isMusicPlaying = false;
|
||
DataPersistence.setLastTimeRoomId("");
|
||
SCGiftVapSvgaManager().stopPlayback();
|
||
SCRoomUtils.closeAllDialogs();
|
||
SCGlobalConfig.resetVisualEffectSwitchesToRecommendedDefaults();
|
||
}
|
||
|
||
void _clearRoomContributionLevelData([BuildContext? targetContext]) {
|
||
final currentContext = targetContext ?? context;
|
||
if (currentContext == null || !currentContext.mounted) {
|
||
return;
|
||
}
|
||
Provider.of<SocialChatRoomManager>(
|
||
currentContext,
|
||
listen: false,
|
||
).clearContributionLevelData();
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_stopRoomStatePolling();
|
||
unawaited(releaseRtcEngineForAppTermination());
|
||
super.dispose();
|
||
}
|
||
|
||
void toggleRemoteAudioMuteForAllUsers() {
|
||
roomIsMute = !roomIsMute;
|
||
engine?.muteAllRemoteAudioStreams(roomIsMute);
|
||
notifyListeners();
|
||
}
|
||
|
||
///点击的位置
|
||
void clickSite(num index, {SocialChatUserProfile? clickUser}) {
|
||
if (_shouldDeferSeatInteractionForRoomStartup) {
|
||
_deferRoomStartupSeatAction(
|
||
_PendingRoomStartupSeatAction.clickSeat(
|
||
index: index,
|
||
clickUser: clickUser,
|
||
),
|
||
);
|
||
return;
|
||
}
|
||
|
||
if (_handleDirectSeatInteraction(index, clickUser: clickUser)) {
|
||
return;
|
||
}
|
||
|
||
if (index == -1) {
|
||
if (clickUser != null) {
|
||
if (clickUser.id ==
|
||
AccountStorage().getCurrentUser()?.userProfile?.id) {
|
||
///是自己,直接打开资料卡
|
||
showBottomInCenterDialog(
|
||
context!,
|
||
RoomUserInfoCard(userId: clickUser.id),
|
||
);
|
||
} else {
|
||
showBottomInBottomDialog(
|
||
context!,
|
||
EmptyMaiSelect(index: index, clickUser: clickUser),
|
||
);
|
||
}
|
||
}
|
||
} else {
|
||
SocialChatUserProfile? roomWheatUser = micAtIndexForDisplay(index)?.user;
|
||
showBottomInBottomDialog(
|
||
context!,
|
||
EmptyMaiSelect(index: index, clickUser: roomWheatUser),
|
||
);
|
||
// if (roomWheatUser != null) {
|
||
// ///麦上有人
|
||
// if (roomWheatUser.id ==
|
||
// AccountStorage().getCurrentUser()?.userProfile?.id) {
|
||
// ///是自己
|
||
// showBottomInBottomDialog(
|
||
// context!,
|
||
// EmptyMaiSelect(index: index, clickUser: roomWheatUser),
|
||
// );
|
||
// } else {
|
||
// showBottomInBottomDialog(
|
||
// context!,
|
||
// RoomUserInfoCard(userId: roomWheatUser.id),
|
||
// );
|
||
// }
|
||
// } else {
|
||
// showBottomInBottomDialog(
|
||
// context!,
|
||
// EmptyMaiSelect(index: index, clickUser: roomWheatUser),
|
||
// );
|
||
// }
|
||
}
|
||
}
|
||
|
||
bool _handleDirectSeatInteraction(
|
||
num index, {
|
||
SocialChatUserProfile? clickUser,
|
||
}) {
|
||
final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id;
|
||
final isRoomAdmin = isFz() || isGL();
|
||
|
||
if (index == -1) {
|
||
if (clickUser == null || (clickUser.id ?? '').isEmpty) {
|
||
return false;
|
||
}
|
||
|
||
if (clickUser.id == currentUserId || !isRoomAdmin) {
|
||
_openRoomUserInfoCard(clickUser.id);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
final seat = micAtIndexForDisplay(index);
|
||
final seatUser = seat?.user;
|
||
if (seatUser == null) {
|
||
if (!(seat?.micLock ?? false) && !isRoomAdmin) {
|
||
shangMai(index);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
if (seatUser.id == currentUserId) {
|
||
if (isRoomAdmin) {
|
||
return false;
|
||
}
|
||
_openRoomUserInfoCard(seatUser.id);
|
||
return true;
|
||
}
|
||
|
||
if (!isRoomAdmin && (seatUser.id ?? '').isNotEmpty) {
|
||
_openRoomUserInfoCard(seatUser.id);
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
void _refreshMicListSilently({bool notifyIfUnchanged = false}) {
|
||
retrieveMicrophoneList(
|
||
notifyIfUnchanged: notifyIfUnchanged,
|
||
).catchError((_) {});
|
||
}
|
||
|
||
void _clearUserFromSeats(String? userId, {num? exceptIndex}) {
|
||
final normalizedUserId = (userId ?? "").trim();
|
||
if (normalizedUserId.isEmpty) {
|
||
return;
|
||
}
|
||
roomWheatMap.forEach((seatIndex, seat) {
|
||
if (seat.user?.id != normalizedUserId || seatIndex == exceptIndex) {
|
||
return;
|
||
}
|
||
roomWheatMap[seatIndex] = seat.copyWith(clearUser: true);
|
||
});
|
||
}
|
||
|
||
void _openRoomUserInfoCard(String? userId) {
|
||
final normalizedUserId = (userId ?? '').trim();
|
||
if (normalizedUserId.isEmpty) {
|
||
return;
|
||
}
|
||
showBottomInCenterDialog(
|
||
context!,
|
||
RoomUserInfoCard(userId: normalizedUserId),
|
||
);
|
||
}
|
||
|
||
addSoundVoiceChangeListener(OnSoundVoiceChange onSoundVoiceChange) {
|
||
_onSoundVoiceChangeList.add(onSoundVoiceChange);
|
||
debugPrint('添加监听:${_onSoundVoiceChangeList.length}');
|
||
}
|
||
|
||
removeSoundVoiceChangeListener(OnSoundVoiceChange onSoundVoiceChange) {
|
||
_onSoundVoiceChangeList.remove(onSoundVoiceChange);
|
||
debugPrint('删除监听:${_onSoundVoiceChangeList.length}');
|
||
}
|
||
|
||
void shangMai(num index, {String? eventType, String? inviterId}) async {
|
||
if (_shouldDeferSeatInteractionForRoomStartup) {
|
||
_deferRoomStartupSeatAction(
|
||
_PendingRoomStartupSeatAction.goUpMic(
|
||
index: index,
|
||
eventType: eventType,
|
||
inviterId: inviterId,
|
||
),
|
||
);
|
||
return;
|
||
}
|
||
|
||
var myUser = AccountStorage().getCurrentUser()?.userProfile;
|
||
if (currenRoom?.roomProfile?.roomSetting?.touristMike ?? false) {
|
||
} else {
|
||
if (isTourists()) {
|
||
SCTts.show(
|
||
SCAppLocalizations.of(context!)!.touristsAreNotAllowedToGoOnTheMic,
|
||
);
|
||
return;
|
||
}
|
||
}
|
||
|
||
final shouldShowRetryLoading = _selfMicGoUpConsecutiveFailureCount > 0;
|
||
if (_isSelfMicActionInFlight) {
|
||
if (shouldShowRetryLoading) {
|
||
_showSelfMicGoUpRetryLoading();
|
||
}
|
||
return;
|
||
}
|
||
if (shouldShowRetryLoading) {
|
||
_showSelfMicGoUpRetryLoading();
|
||
}
|
||
_isSelfMicActionInFlight = true;
|
||
try {
|
||
var micGoUpRes = await SCChatRoomRepository().micGoUp(
|
||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||
index,
|
||
eventType: eventType,
|
||
inviterId: inviterId,
|
||
);
|
||
|
||
final targetIndex = micGoUpRes.micIndex ?? index;
|
||
final switched = await _switchAgoraToBroadcaster(
|
||
publisherToken: micGoUpRes.roomToken ?? "",
|
||
muted: (micGoUpRes.micMute ?? false) || isMic,
|
||
);
|
||
if (!switched) {
|
||
await _switchAgoraToAudience();
|
||
try {
|
||
await SCChatRoomRepository().micGoDown(
|
||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||
targetIndex,
|
||
);
|
||
} catch (downError) {
|
||
debugPrint(
|
||
'[Agora] release backend mic after broadcaster switch failed: $downError',
|
||
);
|
||
}
|
||
_clearSelfMicSwitchGuard(clearPreferredIndex: true);
|
||
_clearUserFromSeats(myUser?.id);
|
||
_syncSelfMicRuntimeState();
|
||
notifyListeners();
|
||
_refreshMicListSilently(notifyIfUnchanged: true);
|
||
_handleSelfMicGoUpFailure(
|
||
'Failed to put on the microphone, Agora switch failed',
|
||
);
|
||
return;
|
||
}
|
||
|
||
final previousSelfSeatIndex =
|
||
myUser != null ? userOnMaiInIndex(myUser.id ?? "") : -1;
|
||
final isSeatSwitching =
|
||
previousSelfSeatIndex > -1 && previousSelfSeatIndex != targetIndex;
|
||
if (myUser != null) {
|
||
_clearSelfMicReleaseGuard();
|
||
if (isSeatSwitching) {
|
||
_startSelfMicSwitchGuard(
|
||
sourceIndex: previousSelfSeatIndex,
|
||
targetIndex: targetIndex,
|
||
);
|
||
} else {
|
||
_preferredSelfMicIndex = targetIndex;
|
||
_clearSelfMicSwitchGuard();
|
||
}
|
||
_clearUserFromSeats(myUser.id, exceptIndex: targetIndex);
|
||
final currentSeat = roomWheatMap[targetIndex];
|
||
final currentUser = myUser.copyWith(roles: currenRoom?.entrants?.roles);
|
||
roomWheatMap[targetIndex] =
|
||
currentSeat?.copyWith(
|
||
user: currentUser,
|
||
micMute: micGoUpRes.micMute,
|
||
micLock: micGoUpRes.micLock,
|
||
roomToken: micGoUpRes.roomToken,
|
||
) ??
|
||
MicRes(
|
||
roomId: currenRoom?.roomProfile?.roomProfile?.id,
|
||
micIndex: targetIndex,
|
||
micLock: micGoUpRes.micLock,
|
||
micMute: micGoUpRes.micMute,
|
||
user: currentUser,
|
||
roomToken: micGoUpRes.roomToken,
|
||
);
|
||
}
|
||
_syncSelfMicRuntimeState();
|
||
if (roomWheatMap[targetIndex]?.micMute ?? false) {
|
||
///房主上麦自动解禁麦位
|
||
if (isFz()) {
|
||
await jieJinMai(targetIndex);
|
||
}
|
||
}
|
||
|
||
notifyListeners();
|
||
_refreshMicListSilently(notifyIfUnchanged: true);
|
||
_resetSelfMicGoUpFailureState();
|
||
} catch (ex) {
|
||
if (_isCancelledRequest(ex)) {
|
||
_refreshMicListSilently(notifyIfUnchanged: true);
|
||
return;
|
||
}
|
||
_handleSelfMicGoUpFailure(
|
||
'Failed to put on the microphone, ${_errorMessageFrom(ex)}',
|
||
);
|
||
} finally {
|
||
_dismissSelfMicGoUpRetryLoading();
|
||
_isSelfMicActionInFlight = false;
|
||
}
|
||
}
|
||
|
||
xiaMai(num index) async {
|
||
if (_isSelfMicActionInFlight) {
|
||
return;
|
||
}
|
||
_isSelfMicActionInFlight = true;
|
||
try {
|
||
await SCChatRoomRepository().micGoDown(
|
||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||
index,
|
||
);
|
||
|
||
final currentUserId =
|
||
AccountStorage().getCurrentUser()?.userProfile?.id ?? "";
|
||
final currentUserSeatIndex = userOnMaiInIndex(currentUserId);
|
||
if (roomWheatMap[index]?.user?.id == currentUserId) {
|
||
isMic = true;
|
||
}
|
||
_clearSelfMicSwitchGuard(clearPreferredIndex: true);
|
||
if (currentUserSeatIndex > -1) {
|
||
_startSelfMicReleaseGuard(sourceIndex: currentUserSeatIndex);
|
||
} else {
|
||
_clearSelfMicReleaseGuard();
|
||
}
|
||
SCHeartbeatUtils.cancelAnchorTimer();
|
||
|
||
await _switchAgoraToAudience();
|
||
_clearUserFromSeats(currentUserId);
|
||
_syncSelfMicRuntimeState();
|
||
notifyListeners();
|
||
requestMicrophoneListRefresh(
|
||
notifyIfUnchanged: false,
|
||
minInterval: const Duration(milliseconds: 350),
|
||
);
|
||
} catch (ex) {
|
||
if (_isCancelledRequest(ex)) {
|
||
_refreshMicListSilently(notifyIfUnchanged: true);
|
||
return;
|
||
}
|
||
SCTts.show('Failed to leave the microphone, ${_errorMessageFrom(ex)}');
|
||
} finally {
|
||
_isSelfMicActionInFlight = false;
|
||
}
|
||
}
|
||
|
||
///踢人下麦
|
||
killXiaMai(String userId) {
|
||
try {
|
||
roomWheatMap.forEach((k, v) async {
|
||
if (v.user?.id == userId) {
|
||
var canKill = await SCChatRoomRepository().kickOffMicrophone(
|
||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||
userId,
|
||
);
|
||
if (canKill) {
|
||
await SCChatRoomRepository().micKill(
|
||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||
k,
|
||
);
|
||
Provider.of<RtmProvider>(context!, listen: false).dispatchMessage(
|
||
Msg(
|
||
groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount,
|
||
msg: userId,
|
||
type: SCRoomMsgType.killXiaMai,
|
||
),
|
||
addLocal: false,
|
||
);
|
||
roomWheatMap[k]?.setUser = null;
|
||
notifyListeners();
|
||
} else {
|
||
SCTts.show("cant kill the user");
|
||
}
|
||
}
|
||
});
|
||
} catch (e) {
|
||
SCTts.show("kill the user fail");
|
||
debugPrint('踢人下麦失败');
|
||
}
|
||
}
|
||
|
||
///自己是否在麦上
|
||
bool isOnMai() {
|
||
final currentUserId =
|
||
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
|
||
if (currentUserId.isEmpty) {
|
||
return false;
|
||
}
|
||
for (final entry in roomWheatMap.entries) {
|
||
if ((micAtIndexForDisplay(entry.key)?.user?.id ?? "").trim() ==
|
||
currentUserId) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
///自己是否在指定的麦上
|
||
bool isOnMaiInIndex(num index) {
|
||
return (micAtIndexForDisplay(index)?.user?.id ?? "").trim() ==
|
||
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
|
||
}
|
||
|
||
///点击的用户在哪个麦上
|
||
num userOnMaiInIndex(String userId) {
|
||
final normalizedUserId = userId.trim();
|
||
if (normalizedUserId.isEmpty) {
|
||
return -1;
|
||
}
|
||
num index = -1;
|
||
roomWheatMap.forEach((k, value) {
|
||
final visibleSeat =
|
||
normalizedUserId ==
|
||
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "")
|
||
.trim()
|
||
? micAtIndexForDisplay(k)
|
||
: value;
|
||
if ((visibleSeat?.user?.id ?? "").trim() == normalizedUserId) {
|
||
index = k;
|
||
}
|
||
});
|
||
return index;
|
||
}
|
||
|
||
///座位上的用户是否开麦
|
||
bool userOnMaiInIndexOfMute(String userId) {
|
||
bool micMute = false;
|
||
roomWheatMap.forEach((k, value) {
|
||
if (value.user?.id == userId) {
|
||
value.micMute;
|
||
}
|
||
});
|
||
return micMute;
|
||
}
|
||
|
||
///是否是房主
|
||
bool isFz() {
|
||
return currenRoom?.entrants?.roles == SCRoomRolesType.HOMEOWNER.name;
|
||
}
|
||
|
||
///是否是管理
|
||
bool isGL() {
|
||
return currenRoom?.entrants?.roles == SCRoomRolesType.ADMIN.name;
|
||
}
|
||
|
||
///是否是会员
|
||
bool isHY() {
|
||
return currenRoom?.entrants?.roles == SCRoomRolesType.MEMBER.name;
|
||
}
|
||
|
||
///是否是游客
|
||
bool isTourists() {
|
||
return currenRoom?.entrants?.roles == SCRoomRolesType.TOURIST.name;
|
||
}
|
||
|
||
///麦位变动
|
||
void micChange(List<MicRes>? mics) {
|
||
if (mics == null || mics.isEmpty) {
|
||
return;
|
||
}
|
||
_applyMicSnapshot(_buildMicMap(mics));
|
||
}
|
||
|
||
///找一个空麦位 -1表示没有空位
|
||
num findWheat() {
|
||
for (var entry in roomWheatMap.entries) {
|
||
if (entry.value.user == null && !entry.value.micLock!) {
|
||
return entry.key;
|
||
}
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
Future<bool> loadRoomInfo(String roomId) async {
|
||
try {
|
||
debugPrint("[Room Cover Sync] loadRoomInfo start roomId=$roomId");
|
||
final value = await SCChatRoomRepository().specific(roomId);
|
||
final currentRoomProfile = currenRoom?.roomProfile?.roomProfile;
|
||
debugPrint(
|
||
"[Room Cover Sync] loadRoomInfo fetched roomId=${value.id ?? roomId} roomCover=${value.roomCover ?? ""} roomBackground=${value.roomBackground ?? ""} roomName=${value.roomName ?? ""}",
|
||
);
|
||
currenRoom = currenRoom?.copyWith(
|
||
roomProfile: currenRoom?.roomProfile?.copyWith(
|
||
roomCounter: value.counter ?? currenRoom?.roomProfile?.roomCounter,
|
||
roomSetting: value.setting ?? currenRoom?.roomProfile?.roomSetting,
|
||
roomProfile: currentRoomProfile?.copyWith(
|
||
roomCover: _preferNonEmpty(
|
||
value.roomCover,
|
||
currentRoomProfile.roomCover,
|
||
),
|
||
roomBackground: _preferNonEmpty(
|
||
value.roomBackground,
|
||
currentRoomProfile.roomBackground,
|
||
),
|
||
roomName: _preferNonEmpty(
|
||
value.roomName,
|
||
currentRoomProfile.roomName,
|
||
),
|
||
roomDesc: _preferNonEmpty(
|
||
value.roomDesc,
|
||
currentRoomProfile.roomDesc,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
debugPrint(
|
||
"[Room Cover Sync] loadRoomInfo applied roomId=$roomId roomCover=${currenRoom?.roomProfile?.roomProfile?.roomCover ?? ""} roomBackground=${currenRoom?.roomProfile?.roomProfile?.roomBackground ?? ""}",
|
||
);
|
||
|
||
///如果是游客禁止上麦,已经在麦上的游客需要下麦
|
||
bool touristMike =
|
||
currenRoom?.roomProfile?.roomSetting?.touristMike ?? false;
|
||
if (!touristMike && isTourists()) {
|
||
num index = userOnMaiInIndex(
|
||
AccountStorage().getCurrentUser()?.userProfile?.id ?? "",
|
||
);
|
||
if (index > -1) {
|
||
xiaMai(index);
|
||
}
|
||
}
|
||
notifyListeners();
|
||
return true;
|
||
} catch (e) {
|
||
await resetLocalRoomState(fallbackRtmProvider: rtmProvider);
|
||
SCTts.show("Room not exists");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
///解锁麦位
|
||
Future<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 _switchAgoraToBroadcaster(
|
||
publisherToken: roomWheatMap[index]?.roomToken ?? "",
|
||
muted: true,
|
||
);
|
||
}
|
||
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 _switchAgoraToBroadcaster(
|
||
publisherToken: roomWheatMap[index]?.roomToken ?? "",
|
||
muted: false,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
var mic = roomWheatMap[index];
|
||
if (mic != null) {
|
||
roomWheatMap[index] = mic.copyWith(micMute: false);
|
||
notifyListeners();
|
||
}
|
||
_refreshMicListSilently();
|
||
}
|
||
|
||
void addOnlineUser(String groupId, SocialChatUserProfile user) {
|
||
if (groupId != currenRoom?.roomProfile?.roomProfile?.roomAccount) {
|
||
return;
|
||
}
|
||
final mergedUser = _mergeKnownVipIntoProfile(user) ?? user;
|
||
bool isExtOnlineList = false;
|
||
for (var us in onlineUsers) {
|
||
if (us.id == mergedUser.id) {
|
||
isExtOnlineList = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!isExtOnlineList) {
|
||
Provider.of<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) {
|
||
currenRoom?.roomProfile?.setRoomSetting(roomSetting);
|
||
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;
|
||
}
|
||
engine?.startAudioMixing(
|
||
filePath: localPath,
|
||
loopback: false,
|
||
cycle: cycle,
|
||
);
|
||
}
|
||
|
||
void adjustRecordingSignalVolume(int volume) {
|
||
engine?.adjustRecordingSignalVolume(volume);
|
||
}
|
||
|
||
void setRoomMusicMixingStateListener(RoomMusicMixingStateListener? listener) {
|
||
_roomMusicMixingStateListener = listener;
|
||
}
|
||
|
||
void setRoomMusicMicRouteChangedListener(
|
||
RoomMusicMicRouteChangedListener? listener,
|
||
) {
|
||
_roomMusicMicRouteChangedListener = listener;
|
||
}
|
||
|
||
Future<void> startRoomMusicAudioMixing({
|
||
required String filePath,
|
||
required bool loopback,
|
||
int cycle = 1,
|
||
int startPos = 0,
|
||
}) async {
|
||
if (filePath.trim().isEmpty) {
|
||
return;
|
||
}
|
||
await engine?.startAudioMixing(
|
||
filePath: filePath,
|
||
loopback: loopback,
|
||
cycle: cycle,
|
||
startPos: startPos,
|
||
);
|
||
}
|
||
|
||
Future<void> pauseRoomMusicAudioMixing() async {
|
||
await engine?.pauseAudioMixing();
|
||
}
|
||
|
||
Future<void> resumeRoomMusicAudioMixing() async {
|
||
await engine?.resumeAudioMixing();
|
||
}
|
||
|
||
Future<void> stopRoomMusicAudioMixing() async {
|
||
await engine?.stopAudioMixing();
|
||
}
|
||
|
||
Future<void> setRoomMusicAudioMixingPosition(int positionMs) async {
|
||
await engine?.setAudioMixingPosition(positionMs);
|
||
}
|
||
|
||
Future<int> getRoomMusicAudioMixingPosition() async {
|
||
return await engine?.getAudioMixingCurrentPosition() ?? 0;
|
||
}
|
||
|
||
Future<int> getRoomMusicAudioMixingDuration() async {
|
||
return await engine?.getAudioMixingDuration() ?? 0;
|
||
}
|
||
|
||
Future<void> setRoomMusicAudioMixingVolume(int volume) async {
|
||
final safeVolume = volume.clamp(0, 100).toInt();
|
||
await engine?.adjustAudioMixingPlayoutVolume(safeVolume);
|
||
await engine?.adjustAudioMixingPublishVolume(safeVolume);
|
||
}
|
||
}
|