yumi-flutter/lib/services/audio/rtc_manager.dart
roxy bd6d35ee1b 🚀 UI
2026-05-14 17:10:53 +08:00

4367 lines
135 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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/models/message/room_rocket_launch_broadcast_message.dart';
import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart';
import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.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/audio/voice_room_foreground_service.dart';
import 'package:yumi/services/auth/user_profile_manager.dart';
import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart';
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
import 'package:provider/provider.dart';
import 'package:yumi/ui_kit/components/sc_tts.dart';
import 'package:yumi/app/constants/sc_global_config.dart';
import 'package:yumi/shared/tools/sc_lk_dialog_util.dart';
import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart';
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
import 'package:yumi/shared/business_logic/models/res/join_room_res.dart'
hide PropsResources;
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
import 'package:yumi/shared/business_logic/models/res/mic_res.dart';
import 'package:yumi/shared/business_logic/models/res/follow_room_res.dart'
as follow_room;
import 'package:yumi/shared/business_logic/models/res/my_room_res.dart'
as my_room;
import 'package:yumi/shared/business_logic/models/res/room_res.dart'
hide PropsResources;
import 'package:yumi/modules/room/voice_room_route.dart';
import 'package:yumi/ui_kit/widgets/room/empty_mai_select.dart';
import 'package:yumi/ui_kit/widgets/room/room_user_info_card.dart';
import '../../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_config_res.dart';
import '../../shared/business_logic/models/res/sc_room_red_packet_list_res.dart';
import '../../shared/business_logic/models/res/sc_room_rocket_status_res.dart';
import '../../shared/business_logic/models/res/sc_room_theme_list_res.dart';
import '../../shared/business_logic/models/res/sc_vip_res.dart';
import '../../shared/tools/sc_room_profile_cache.dart';
import '../../ui_kit/components/sc_float_ichart.dart';
import '../../ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart';
import '../../ui_kit/widgets/room/rocket/room_rocket_reward_dialog.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 String _roomRocketRewardDialogTag = 'showRoomRocketRewardDialog';
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 Duration _roomMusicPublishingStateTtl = Duration(seconds: 15);
static const String _roomStartupSeatLoadingTag =
'roomStartupSeatInteractionLoading';
static const String _selfMicGoUpRetryLoadingTag = 'selfMicGoUpRetryLoading';
static const int _selfMicGoUpFailureLimit = 3;
static const List<Duration> _roomRedPacketEntryRefreshDelays = [
Duration.zero,
Duration(seconds: 2),
Duration(seconds: 6),
];
bool needUpDataUserInfo = false;
bool _roomVisualEffectsEnabled = false;
bool _voiceRoomRouteVisible = false;
bool _isExitingCurrentVoiceRoomSession = false;
bool _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;
Timer? _roomRocketLaunchAnimationTimer;
Timer? _roomRedPacketPresenceTimer;
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? _lastAppliedRecordingSignalMuted;
int? _lastAppliedRecordingSignalVolume;
String? _lastAppliedRtcToken;
bool? _lastScheduledVoiceLiveOnMic;
String? _lastScheduledVoiceLiveRoomId;
String? _lastScheduledAnchorRoomId;
///当前所在房间
JoinRoomRes? currenRoom;
/// 声音音量变化监听
final List<OnSoundVoiceChange> _onSoundVoiceChangeList = [];
///麦位
Map<num, MicRes> roomWheatMap = {};
final Map<num, int> _seatEmojiEventVersions = {};
int _emojiPlaybackEventVersion = 0;
final Set<String> _roomMusicPublishingUserIds = <String>{};
final Map<String, Timer> _roomMusicPublishingClearTimers = <String, Timer>{};
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;
bool isUserPublishingRoomMusic(String? userId) {
final normalizedUserId = (userId ?? "").trim();
return normalizedUserId.isNotEmpty &&
_roomMusicPublishingUserIds.contains(normalizedUserId);
}
///房间红包列表
List<SCRoomRedPacketListRes> redPacketList = [];
num roomTaskClaimableCount = 0;
initializeRealTimeCommunicationManager(BuildContext context) {
this.context = context;
}
bool get roomVisualEffectsEnabled => _roomVisualEffectsEnabled;
bool get isVoiceRoomRouteVisible => _voiceRoomRouteVisible;
bool get isExitingCurrentVoiceRoomSession =>
_isExitingCurrentVoiceRoomSession;
RoomStartupStatus get roomStartupStatus => _roomStartupStatus;
RoomStartupFailureType get roomStartupFailureType => _roomStartupFailureType;
int get roomStartupFailureToken => _roomStartupFailureToken;
int? get previewRoomSeatCount => _previewRoomSeatCount;
bool get shouldShowRoomVisualEffects =>
currenRoom != null && _roomVisualEffectsEnabled;
void _startVoiceRoomForegroundService() {
final roomProfile = currenRoom?.roomProfile?.roomProfile;
final roomName = (roomProfile?.roomName ?? "").trim();
final roomAccount = (roomProfile?.roomAccount ?? "").trim();
unawaited(
SCVoiceRoomForegroundService.start(
roomName: roomName.isNotEmpty ? roomName : roomAccount,
),
);
}
void _stopVoiceRoomForegroundService() {
unawaited(SCVoiceRoomForegroundService.stop());
}
bool get _shouldDeferSeatInteractionForRoomStartup =>
_roomStartupStatus == RoomStartupStatus.loading;
void setRoomVisualEffectsEnabled(bool enabled) {
if (_roomVisualEffectsEnabled == enabled) {
return;
}
if (!enabled) {
_roomEntryEffectTimer?.cancel();
_roomEntryEffectTimer = null;
_roomRocketLaunchAnimationTimer?.cancel();
_roomRocketLaunchAnimationTimer = null;
}
_roomVisualEffectsEnabled = enabled;
notifyListeners();
}
void setVoiceRoomRouteVisible(bool visible) {
_voiceRoomRouteVisible = visible;
}
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;
}
MicRes? _currentUserMicSeat() {
final currentUserId =
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
if (currentUserId.isEmpty) {
return null;
}
for (final mic in roomWheatMap.values) {
if ((mic.user?.id ?? "").trim() == currentUserId) {
return mic;
}
}
return null;
}
void _syncSelfMicRuntimeState() {
final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id;
if ((currentUserId ?? "").isEmpty) {
return;
}
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
final currentUserMic = _currentUserMicSeat();
if (currentUserMic == null) {
_clearSelfMicSwitchGuard(clearPreferredIndex: true);
_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,
publisherToken: currentUserMic.roomToken,
);
_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,
String? publisherToken,
}) {
if (clientRole != ClientRoleType.clientRoleBroadcaster) {
_agoraRoleIsBroadcaster = false;
}
if (engine == null) {
_lastAppliedClientRole = null;
_lastAppliedLocalAudioMuted = null;
_lastAppliedRecordingSignalMuted = null;
_lastAppliedRecordingSignalVolume = null;
_lastAppliedRtcToken = null;
return;
}
final normalizedPublisherToken = (publisherToken ?? "").trim();
if (clientRole == ClientRoleType.clientRoleBroadcaster &&
normalizedPublisherToken.isNotEmpty &&
_lastAppliedRtcToken != normalizedPublisherToken) {
engine?.renewToken(normalizedPublisherToken);
_lastAppliedRtcToken = normalizedPublisherToken;
}
if (_lastAppliedClientRole != clientRole) {
engine?.setClientRole(role: clientRole);
_lastAppliedClientRole = clientRole;
}
final shouldPublishLocalAudio =
clientRole == ClientRoleType.clientRoleBroadcaster;
final localAudioMuted = !shouldPublishLocalAudio;
if (_lastAppliedLocalAudioMuted != localAudioMuted) {
engine?.muteLocalAudioStream(localAudioMuted);
_lastAppliedLocalAudioMuted = localAudioMuted;
}
final recordingSignalMuted = !shouldPublishLocalAudio;
if (_lastAppliedRecordingSignalMuted != recordingSignalMuted) {
engine?.muteRecordingSignal(recordingSignalMuted);
_lastAppliedRecordingSignalMuted = recordingSignalMuted;
}
final recordingSignalVolume =
shouldPublishLocalAudio ? (muted ? 0 : 100) : 0;
if (_lastAppliedRecordingSignalVolume != recordingSignalVolume) {
adjustRecordingSignalVolume(recordingSignalVolume);
_lastAppliedRecordingSignalVolume = recordingSignalVolume;
}
}
void _resetHeartbeatTracking() {
_lastScheduledVoiceLiveOnMic = null;
_lastScheduledVoiceLiveRoomId = null;
_lastScheduledAnchorRoomId = null;
}
void _resetLocalAudioRuntimeTracking() {
_lastAppliedClientRole = null;
_lastAppliedLocalAudioMuted = null;
_lastAppliedRecordingSignalMuted = null;
_lastAppliedRecordingSignalVolume = null;
_lastAppliedRtcToken = null;
}
void _applyMicSnapshot(
Map<num, MicRes> nextMap, {
bool notifyIfUnchanged = true,
}) {
final changed = !_sameMicMaps(roomWheatMap, nextMap);
roomWheatMap = nextMap;
_syncSelfMicRuntimeState();
final roomMusicChanged = _pruneRoomMusicPublishingUsersToMicSeats();
if (changed || notifyIfUnchanged || roomMusicChanged) {
notifyListeners();
}
}
bool _pruneRoomMusicPublishingUsersToMicSeats() {
if (_roomMusicPublishingUserIds.isEmpty) {
return false;
}
final seatedUserIds =
roomWheatMap.values
.map((seat) => (seat.user?.id ?? "").trim())
.where((userId) => userId.isNotEmpty)
.toSet();
final removedUserIds =
_roomMusicPublishingUserIds
.where((userId) => !seatedUserIds.contains(userId))
.toList();
for (final userId in removedUserIds) {
_roomMusicPublishingUserIds.remove(userId);
_roomMusicPublishingClearTimers.remove(userId)?.cancel();
}
return removedUserIds.isNotEmpty;
}
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) {}
if (clearMicSeats) {
roomWheatMap.clear();
notifyListeners();
}
_resyncHeartbeatFromAgoraState();
}
Future<void> _leaveAgoraRoomForExit() async {
final rtcEngine = engine;
_resetAgoraTracking();
if (rtcEngine == null) {
return;
}
try {
await rtcEngine.leaveChannel();
} catch (error) {}
}
Future<void> _handleAgoraJoinFailed(
Object error, {
required bool exitRoom,
}) async {
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;
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;
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) {}
}
_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) {
if (throwOnError) {
rethrow;
}
return;
}
await _handleAgoraJoinFailed(e, exitRoom: !throwOnError);
if (throwOnError) {
rethrow;
}
} 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);
_lastAppliedRtcToken = publisherToken.trim();
}
await engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
await engine?.muteLocalAudioStream(false);
await engine?.muteRecordingSignal(false);
await engine?.adjustRecordingSignalVolume(muted ? 0 : 100);
_lastAppliedClientRole = ClientRoleType.clientRoleBroadcaster;
_lastAppliedLocalAudioMuted = false;
_lastAppliedRecordingSignalMuted = false;
_lastAppliedRecordingSignalVolume = muted ? 0 : 100;
_agoraRoleIsBroadcaster = true;
_resyncHeartbeatFromAgoraState();
return true;
} catch (error) {
_agoraRoleIsBroadcaster = false;
_resyncHeartbeatFromAgoraState();
return false;
}
}
Future<bool> _switchAgoraToAudience() async {
try {
await engine?.setClientRole(role: ClientRoleType.clientRoleAudience);
await engine?.muteLocalAudioStream(true);
await engine?.muteRecordingSignal(true);
await engine?.adjustRecordingSignalVolume(0);
_lastAppliedClientRole = ClientRoleType.clientRoleAudience;
_lastAppliedLocalAudioMuted = true;
_lastAppliedRecordingSignalMuted = true;
_lastAppliedRecordingSignalVolume = 0;
_lastAppliedRtcToken = null;
_agoraRoleIsBroadcaster = false;
_resyncHeartbeatFromAgoraState();
return true;
} catch (error) {
return false;
}
}
Future<void> setSelfMicMutedFromBottom(bool muted) async {
isMic = muted;
notifyListeners();
final currentUserId =
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
final seatIndex = userOnMaiInIndex(currentUserId);
if (seatIndex < 0) {
notifyListeners();
return;
}
final seat = roomWheatMap[seatIndex];
final seatMuted = seat?.micMute ?? false;
if (seatMuted || muted) {
if (_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) {
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) {
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) {},
// 监听远端用户离开
onUserOffline: (connection, 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) {
await SCVoiceRoomForegroundService.stop();
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) {}
_resetAgoraTracking();
try {
await rtcEngine.release();
} catch (e) {}
await SCVoiceRoomForegroundService.stop();
}
void initializeAudioVolumeIndicationCallback(
RtcConnection connection,
List<AudioVolumeInfo> speakers,
int speakerNumber,
int totalVolume,
) {
// if (user == null) {
// throw "本地User尚未初始化";
// }
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 {
SCFloatIchart().remove();
int nextTime = DateTime.now().millisecondsSinceEpoch;
if (nextTime - startTime < 1000) {
//频繁点击
return;
}
startTime = nextTime;
if (clearRoomData) {
_clearData();
}
bool hasPermission = await SCPermissionUtils.checkMicrophonePermission();
if (!hasPermission) {
SCTts.show('Microphone permission is denied.');
throw ArgumentError('Microphone permission is denied.');
}
if (roomId == currenRoom?.roomProfile?.roomProfile?.id) {
if (_currentRoomIsEntryPreview ||
_roomStartupStatus == RoomStartupStatus.loading) {
if (!context.mounted) {
return;
}
_startVoiceRoomForegroundService();
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();
final groupId = currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "";
if (groupId.isNotEmpty) {
unawaited(
rtmProvider?.loadRoomHistoryMessages(groupId, count: 20) ??
Future<void>.value(),
);
}
retrieveMicrophoneList();
fetchOnlineUsersList();
_startRoomStatePolling();
_setRoomStartupReady();
setRoomVisualEffectsEnabled(true);
_startRoomRedPacketPresenceHeartbeat(roomId);
_refreshRoomRedPacketListAfterEntry(roomId: roomId);
_startVoiceRoomForegroundService();
VoiceRoomRoute.openVoiceRoom(context);
} else {
if (currenRoom != null) {
_switchAwayFromCurrentRoomForNewEntry();
}
if (!context.mounted) {
return;
}
rtmProvider = Provider.of<RtmProvider>(context, listen: false);
_clearRoomContributionLevelData(context);
rtmProvider?.seedSystemRoomTips(
SCAppLocalizations.of(context)!.systemRoomTips,
);
_setRoomStartupLoading();
setRoomVisualEffectsEnabled(true);
_applyRoomEntryPreview(
roomId,
previewData: previewData,
previewSeatCount: previewSeatCount,
);
_startVoiceRoomForegroundService();
VoiceRoomRoute.openVoiceRoom(context);
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);
_startVoiceRoomForegroundService();
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;
}
_stopVoiceRoomForegroundService();
_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)) {
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 && joinResult.code != 10013)) {
_stopVoiceRoomForegroundService();
_setRoomStartupFailed(RoomStartupFailureType.im);
return;
}
rtmProvider?.seedSystemRoomTips(
SCAppLocalizations.of(context!)!.systemRoomTips,
groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "",
);
rtmProvider?.addMsg(
Msg(
groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "",
msg: currenRoom?.roomProfile?.roomProfile?.roomDesc,
type: SCRoomMsgType.systemTips,
),
);
unawaited(
rtmProvider?.loadRoomHistoryMessages(groupId, count: 20) ??
Future<void>.value(),
);
_refreshRoomRedPacketListAfterEntry(roomId: roomId);
await Future.wait([microphoneListFuture, onlineUsersFuture]);
if (!_isActiveRoomStartup(entryRequestSerial, roomId)) {
return;
}
_startRoomStatePolling();
socialChatRoomManager.fetchContributionLevelData(roomId);
failureType = RoomStartupFailureType.rtc;
final 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();
_startRoomRedPacketPresenceHeartbeat(roomId);
if (!_isActiveRoomStartup(entryRequestSerial, roomId)) {
return;
}
_setRoomStartupReady();
_refreshRoomRedPacketListAfterEntry(roomId: roomId);
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)) {
return;
}
_stopVoiceRoomForegroundService();
_setRoomStartupFailed(failureType);
}
}
String _firstNonBlankEntryResource(Iterable<String?> values) {
for (final value in values) {
final text = value?.trim();
if (text != null && text.isNotEmpty) {
return text;
}
}
return "";
}
String _resolveEntryCompensationCover(PropsResources? resource) {
return _firstNonBlankEntryResource([
resource?.roomSendCoverUrl,
resource?.roomOpenedUrl,
resource?.cover,
resource?.roomNotOpenedUrl,
]);
}
void _enqueueLowPerformanceEntryCompensation({
required Msg joinMsg,
required PropsResources resource,
required String roomId,
}) {
if (!SCGlobalConfig.isLowPerformanceDevice ||
!shouldShowRoomVisualEffects ||
roomId.isEmpty) {
return;
}
final coverUrl = _resolveEntryCompensationCover(resource);
OverlayManager().addLowPerformanceCompensationMessage(
SCFloatingMessage(
type: 5,
priority: 950,
userId: joinMsg.user?.id ?? "",
roomId: roomId,
userAvatarUrl: joinMsg.user?.userAvatar ?? "",
userName: joinMsg.user?.userNickname ?? "",
giftUrl: coverUrl,
fallbackUrl: coverUrl,
),
dedupKey: [
"entry",
roomId,
joinMsg.user?.id ?? "",
resource.id ?? "",
].join("|"),
);
}
void _playRoomEntryEffects(
Msg joinMsg, {
required int entryRequestSerial,
required String roomId,
}) {
final entryResource =
AccountStorage().getCurrentUser()?.userProfile?.getMountains();
if (entryResource != null) {
final sourceUrl = entryResource.sourceUrl ?? "";
if (sourceUrl.isNotEmpty &&
SCGlobalConfig.allowsHighCostAnimations &&
SCGlobalConfig.isEntryVehicleAnimation &&
shouldShowRoomVisualEffects &&
_isCurrentRoomEntrySession(entryRequestSerial, roomId)) {
_roomEntryEffectTimer?.cancel();
_roomEntryEffectTimer = Timer(const Duration(milliseconds: 550), () {
_roomEntryEffectTimer = null;
if (!shouldShowRoomVisualEffects ||
!_isCurrentRoomEntrySession(entryRequestSerial, roomId)) {
return;
}
SCGiftVapSvgaManager().play(
sourceUrl,
priority: SCGiftVapSvgaManager.entryEffectPriority,
type: SCGiftVapSvgaManager.entryEffectType,
);
});
} else if (sourceUrl.isNotEmpty &&
SCGlobalConfig.isLowPerformanceDevice &&
shouldShowRoomVisualEffects &&
_isCurrentRoomEntrySession(entryRequestSerial, roomId)) {
_roomEntryEffectTimer?.cancel();
_roomEntryEffectTimer = Timer(const Duration(milliseconds: 550), () {
_roomEntryEffectTimer = null;
if (!shouldShowRoomVisualEffects ||
!_isCurrentRoomEntrySession(entryRequestSerial, roomId)) {
return;
}
_enqueueLowPerformanceEntryCompensation(
joinMsg: joinMsg,
resource: entryResource,
roomId: roomId,
);
});
}
}
if (rtmProvider?.msgUserJoinListener != null) {
rtmProvider?.msgUserJoinListener!(joinMsg);
}
}
void _loadRoomSecondaryData() {
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
SCChatRoomRepository().rocketClaim(roomId).catchError((e) {
return true; // 错误已处理
});
SCChatRoomRepository()
.rocketStatus(roomId)
.then((res) {
roomRocketStatus = res;
notifyListeners();
})
.catchError((e) {});
_loadRoomRocketRewardPopups(roomId);
_refreshRoomRedPacketListAfterEntry();
fetchRoomTaskClaimableCount();
}
void _loadRoomRocketRewardPopups(String roomId) {
if (roomId.trim().isEmpty) {
return;
}
SCChatRoomRepository()
.roomRocketRewardPopups(roomId)
.then((res) {
if (res.records.isEmpty ||
roomId !=
(currenRoom?.roomProfile?.roomProfile?.id ?? "").trim()) {
return;
}
final recordIds = res.records
.map((item) => item.id)
.where((item) => item.isNotEmpty)
.toList(growable: false);
SmartDialog.dismiss(tag: _roomRocketRewardDialogTag);
SmartDialog.show(
tag: _roomRocketRewardDialogTag,
alignment: Alignment.center,
debounce: true,
animationType: SmartAnimationType.fade,
maskColor: Colors.transparent,
clickMaskDismiss: false,
builder:
(_) => RoomRocketRewardDialog(
rewards: RoomRocketApiMapper.rewardItemsFromRecords(
res.records,
),
winners: RoomRocketApiMapper.winnersFromRecords(res.records),
loadingText: '',
useMockFallback: false,
onClose: () {
SmartDialog.dismiss(tag: _roomRocketRewardDialogTag);
if (recordIds.isNotEmpty) {
unawaited(
SCChatRoomRepository().ackRoomRocketRewardPopups(
recordIds,
),
);
}
},
),
);
})
.catchError((_) {});
}
///更新房间火箭信息
void updateRoomRocketConfigurationStatus(SCRoomRocketStatusRes res) {
roomRocketStatus = res;
notifyListeners();
}
///刷新房间火箭信息
Future<void> refreshRoomRocketStatus({String? roomId}) async {
final resolvedRoomId =
(roomId ?? currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
if (resolvedRoomId.isEmpty) {
return;
}
final res = await SCChatRoomRepository().rocketStatus(resolvedRoomId);
if (resolvedRoomId !=
(currenRoom?.roomProfile?.roomProfile?.id ?? "").trim()) {
return;
}
roomRocketStatus = res;
notifyListeners();
}
void handleRoomRocketLaunchBroadcast(
RoomRocketLaunchBroadcastMessage launch,
) {
final currentRoomId =
(currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
if (!launch.isValid || launch.roomId != currentRoomId) {
return;
}
_roomRocketLaunchAnimationTimer?.cancel();
if (shouldShowRoomVisualEffects && isVoiceRoomRouteVisible) {
// TODO(rocket-launch): 发射动画素材还没有,资源到位后在这里接入
// SVGA/VAP/PNG 序列播放。这里先保留完整触发入口和时长控制,
// 避免后续接接口时再改 RTM 分发逻辑。
_roomRocketLaunchAnimationTimer = Timer(launch.displayDuration, () {
_roomRocketLaunchAnimationTimer = null;
unawaited(refreshRoomRocketStatus(roomId: launch.roomId));
});
return;
}
unawaited(refreshRoomRocketStatus(roomId: launch.roomId));
}
///获取在线用户
Future<void> fetchOnlineUsersList({bool notifyIfUnchanged = true}) async {
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
if (roomId.isEmpty ||
_isRefreshingOnlineUsers ||
_disableOnlineUsersRefreshForCurrentSession) {
return;
}
_isRefreshingOnlineUsers = true;
try {
final fetchedUsers = await SCChatRoomRepository().roomOnlineUsers(
roomId,
silentErrorToast: true,
);
if (roomId != currenRoom?.roomProfile?.roomProfile?.id) {
return;
}
final mergedUsers =
fetchedUsers.map((user) => _mergeKnownVipIntoProfile(user)!).toList();
final changed = !_sameOnlineUsers(onlineUsers, mergedUsers);
onlineUsers = mergedUsers;
_refreshManagerUsers(onlineUsers);
if (changed || notifyIfUnchanged) {
notifyListeners();
}
} catch (error) {
if (_isMissingPathError(error)) {
_disableOnlineUsersRefreshForCurrentSession = true;
} else {}
} finally {
_isRefreshingOnlineUsers = false;
}
}
Future<void> retrieveMicrophoneList({bool notifyIfUnchanged = true}) async {
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
if (roomId.isEmpty || _disableMicListRefreshForCurrentSession) {
return;
}
if (_isRefreshingMicList) {
_pendingMicListRefresh = true;
_pendingMicListRefreshNotifyIfUnchanged =
_pendingMicListRefreshNotifyIfUnchanged || notifyIfUnchanged;
return;
}
_isRefreshingMicList = true;
_lastMicListRefreshStartedAtMs = DateTime.now().millisecondsSinceEpoch;
try {
final roomWheatList = await SCChatRoomRepository().micList(
roomId,
silentErrorToast: true,
);
if (roomId != currenRoom?.roomProfile?.roomProfile?.id) {
return;
}
final nextMap = _buildMicMap(roomWheatList);
_applyMicSnapshot(nextMap, notifyIfUnchanged: notifyIfUnchanged);
} catch (error) {
if (_isMissingPathError(error)) {
_disableMicListRefreshForCurrentSession = true;
_deferredMicListRefreshTimer?.cancel();
_deferredMicListRefreshTimer = null;
if (notifyIfUnchanged) {
notifyListeners();
}
} else {}
} finally {
_isRefreshingMicList = false;
if (_pendingMicListRefresh) {
_pendingMicListRefresh = false;
final pendingNotifyIfUnchanged =
_pendingMicListRefreshNotifyIfUnchanged;
_pendingMicListRefreshNotifyIfUnchanged = false;
unawaited(
Future<void>.microtask(
() => retrieveMicrophoneList(
notifyIfUnchanged: notifyIfUnchanged || pendingNotifyIfUnchanged,
),
),
);
}
}
}
void fetchRoomTaskClaimableCount() {
SCChatRoomRepository()
.roomTaskClaimableCount()
.then((res) {
roomTaskClaimableCount = res.claimableCount ?? 0;
notifyListeners();
})
.catchError((e) {});
}
Future<List<SCRoomRedPacketListRes>> loadRoomRedPacketList(
int current,
) async {
final roomId = currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? "";
if (roomId.isEmpty) {
return const <SCRoomRedPacketListRes>[];
}
var result = await SCChatRoomRepository().roomRedPacketList(
roomId,
current,
);
final currentRoomId =
currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? "";
if (current == 1 && currentRoomId == roomId) {
redPacketList = result;
notifyListeners();
}
return result;
}
void sendRoomTextMessage(String message, {String? dedupKey}) {
final normalizedMessage = message.trim();
final normalizedDedupKey = dedupKey?.trim() ?? "";
final groupId = currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "";
if (normalizedMessage.isEmpty || groupId.isEmpty) {
return;
}
unawaited(
rtmProvider?.dispatchMessage(
Msg(
groupId: groupId,
msg: normalizedMessage,
type: SCRoomMsgType.text,
role: currenRoom?.entrants?.roles ?? "",
giftBatchId:
normalizedDedupKey.isEmpty ? null : normalizedDedupKey,
user: AccountStorage().getCurrentUser()?.userProfile,
),
) ??
Future<void>.value(),
);
}
void sendRoomRedPacketClaimMessage({
required String packetId,
required num amount,
required String packetSenderName,
}) {
final groupId = currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "";
if (groupId.isEmpty) {
return;
}
final normalizedPacketId = packetId.trim();
final normalizedSenderName = packetSenderName.trim();
final currentUser = AccountStorage().getCurrentUser()?.userProfile;
final currentUserId = (currentUser?.id ?? "").trim();
final dedupKey =
normalizedPacketId.isNotEmpty && currentUserId.isNotEmpty
? 'room-red-packet-claim-$normalizedPacketId-$currentUserId'
: null;
unawaited(
rtmProvider?.dispatchMessage(
Msg(
groupId: groupId,
msg: normalizedSenderName,
type: SCRoomMsgType.roomRedPacketClaim,
role: currenRoom?.entrants?.roles ?? "",
number: amount,
giftBatchId: dedupKey,
user: currentUser,
),
) ??
Future<void>.value(),
);
}
void _refreshRoomRedPacketListAfterEntry({String? roomId}) {
final normalizedRoomId =
roomId?.trim() ??
currenRoom?.roomProfile?.roomProfile?.id?.trim() ??
"";
if (normalizedRoomId.isEmpty) {
return;
}
for (final delay in _roomRedPacketEntryRefreshDelays) {
unawaited(
Future<void>.delayed(delay).then((_) {
return _refreshRoomRedPacketListIfCurrent(normalizedRoomId);
}),
);
}
}
Future<void> _refreshRoomRedPacketListIfCurrent(String roomId) async {
final currentRoomId =
currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? "";
if (roomId.trim().isEmpty || currentRoomId != roomId.trim()) {
return;
}
try {
await loadRoomRedPacketList(1);
} catch (error) {}
}
void _startRoomRedPacketPresenceHeartbeat(String roomId) {
final normalizedRoomId = roomId.trim();
_stopRoomRedPacketPresenceHeartbeat();
if (normalizedRoomId.isEmpty) {
return;
}
void sendHeartbeat() {
if (!_isCurrentRoomId(normalizedRoomId)) {
_stopRoomRedPacketPresenceHeartbeat();
return;
}
SCChatRoomRepository()
.roomRedPacketPresenceHeartbeat(normalizedRoomId)
.catchError((e) {
return SCRoomRedPacketPresenceRes();
});
}
sendHeartbeat();
_roomRedPacketPresenceTimer = Timer.periodic(
const Duration(seconds: 30),
(_) => sendHeartbeat(),
);
}
void _stopRoomRedPacketPresenceHeartbeat() {
if (_roomRedPacketPresenceTimer != null) {
_roomRedPacketPresenceTimer?.cancel();
}
_roomRedPacketPresenceTimer = null;
}
Future exitCurrentVoiceRoomSession(
bool isLogout, {
bool deferRemoteCleanup = true,
}) async {
if (_isExitingCurrentVoiceRoomSession) {
return;
}
_setExitingCurrentVoiceRoomSession(true);
_stopRoomStatePolling();
final groupId = currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "";
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
final isPreviewOnlyExit = _currentRoomIsEntryPreview;
final exitRtmProvider = rtmProvider;
final navigationContext = context;
try {
exitRtmProvider?.msgAllListener = null;
exitRtmProvider?.msgChatListener = null;
exitRtmProvider?.msgGiftListener = null;
SCGiftVapSvgaManager().stopPlayback();
setRoomVisualEffectsEnabled(false);
SCHeartbeatUtils.cancelTimer();
_stopRoomRedPacketPresenceHeartbeat();
exitRtmProvider?.cleanRoomData();
} catch (e) {}
_clearData();
if (!isLogout && navigationContext != null) {
VoiceRoomRoute.popVoiceRoomToPrevious(navigationContext);
}
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();
_stopRoomRedPacketPresenceHeartbeat();
exitRtmProvider?.cleanRoomData();
} catch (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) {
return;
}
await SCHeartbeatUtils.scheduleHeartbeat(
SCHeartbeatStatus.ONLINE.name,
false,
rethrowOnError: true,
);
}),
);
}
if (groupId.isNotEmpty && exitRtmProvider != null) {
tasks.add(
_retryExitNetworkRequest('quit IM group $groupId', () async {
if (_isCurrentRoomGroup(groupId)) {
return;
}
await exitRtmProvider.quitGroup(groupId);
}),
);
}
if (roomId.isNotEmpty) {
tasks.add(
_retryExitNetworkRequest(
'leave room red packet presence $roomId',
() async {
await SCChatRoomRepository().roomRedPacketPresenceLeave(roomId);
},
),
);
tasks.add(
_retryExitNetworkRequest('quit room $roomId', () async {
if (_isCurrentRoomId(roomId)) {
return;
}
final didQuit = await SCAccountRepository().quitRoom(roomId);
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;
if (attempt < _exitNetworkRetryLimit) {
await Future.delayed(_exitNetworkRetryDelay * attempt);
}
}
}
}
bool _isCurrentRoomId(String roomId) {
return roomId.isNotEmpty &&
currenRoom?.roomProfile?.roomProfile?.id == roomId;
}
bool _isCurrentRoomGroup(String groupId) {
return groupId.isNotEmpty &&
currenRoom?.roomProfile?.roomProfile?.roomAccount == groupId;
}
Future<void> resetLocalRoomState({RtmProvider? fallbackRtmProvider}) async {
rtmProvider ??= fallbackRtmProvider;
_stopRoomStatePolling();
try {
SCHeartbeatUtils.cancelTimer();
SCHeartbeatUtils.cancelAnchorTimer();
final groupId = currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "";
if (groupId.isNotEmpty) {
await rtmProvider?.quitGroup(groupId);
}
await _cleanupAgoraRoomState();
await Future.delayed(const Duration(milliseconds: 100));
} catch (e) {
} finally {
rtmProvider?.cleanRoomData();
_clearData();
notifyListeners();
}
}
Future<void> confirmRoomStartupFailureAndLeave({
BuildContext? navigationContext,
}) async {
if (_isHandlingRoomStartupFailure) {
return;
}
_isHandlingRoomStartupFailure = true;
final failedRoom = currenRoom;
final isPreviewOnlyFailure = _currentRoomIsEntryPreview;
final groupId = failedRoom?.roomProfile?.roomProfile?.roomAccount ?? "";
final roomId = failedRoom?.roomProfile?.roomProfile?.id ?? "";
final navigatorContext = navigationContext ?? context;
final navigator =
navigatorContext != null ? Navigator.maybeOf(navigatorContext) : null;
try {
_stopRoomStatePolling();
SCGiftVapSvgaManager().stopPlayback();
SCHeartbeatUtils.cancelTimer();
SCHeartbeatUtils.cancelAnchorTimer();
_stopRoomRedPacketPresenceHeartbeat();
rtmProvider?.cleanRoomData();
if (!isPreviewOnlyFailure && groupId.isNotEmpty) {
await rtmProvider?.quitGroup(groupId);
}
await _cleanupAgoraRoomState(clearMicSeats: true);
await Future.delayed(const Duration(milliseconds: 100));
if (!isPreviewOnlyFailure && roomId.isNotEmpty) {
await SCChatRoomRepository().roomRedPacketPresenceLeave(roomId);
await SCAccountRepository().quitRoom(roomId);
}
} catch (e) {
} finally {
_clearData();
_isHandlingRoomStartupFailure = false;
notifyListeners();
if (navigator != null && navigator.mounted && navigator.canPop()) {
navigator.pop();
}
}
}
///清空列表数据
void _clearData() {
_roomEntryRequestSerial += 1;
_stopVoiceRoomForegroundService();
_roomEntryEffectTimer?.cancel();
_roomEntryEffectTimer = null;
_roomRocketLaunchAnimationTimer?.cancel();
_roomRocketLaunchAnimationTimer = null;
_stopRoomRedPacketPresenceHeartbeat();
_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;
_clearRoomMusicPublishingStates();
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;
_voiceRoomRouteVisible = false;
_isExitingCurrentVoiceRoomSession = false;
isMic = true;
isMusicPlaying = false;
DataPersistence.setLastTimeRoomId("");
SCGiftVapSvgaManager().stopPlayback();
SCRoomUtils.closeAllDialogs();
SCGlobalConfig.resetVisualEffectSwitchesToRecommendedDefaults();
}
void _clearRoomContributionLevelData([BuildContext? targetContext]) {
final currentContext = targetContext ?? context;
resolveSocialChatRoomManager(currentContext).clearContributionLevelData();
}
@override
void dispose() {
_stopRoomStatePolling();
_clearRoomMusicPublishingStates();
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);
}
removeSoundVoiceChangeListener(OnSoundVoiceChange onSoundVoiceChange) {
_onSoundVoiceChangeList.remove(onSoundVoiceChange);
}
void shangMai(num index, {String? eventType, String? inviterId}) async {
if (_shouldDeferSeatInteractionForRoomStartup) {
_deferRoomStartupSeatAction(
_PendingRoomStartupSeatAction.goUpMic(
index: index,
eventType: eventType,
inviterId: inviterId,
),
);
return;
}
var myUser = AccountStorage().getCurrentUser()?.userProfile;
if (currenRoom?.roomProfile?.roomSetting?.touristMike ?? false) {
} else {
if (isTourists()) {
SCTts.show(
SCAppLocalizations.of(context!)!.touristsAreNotAllowedToGoOnTheMic,
);
return;
}
}
final shouldShowRetryLoading = _selfMicGoUpConsecutiveFailureCount > 0;
if (_isSelfMicActionInFlight) {
if (shouldShowRetryLoading) {
_showSelfMicGoUpRetryLoading();
}
return;
}
if (shouldShowRetryLoading) {
_showSelfMicGoUpRetryLoading();
}
_isSelfMicActionInFlight = true;
try {
var micGoUpRes = await SCChatRoomRepository().micGoUp(
currenRoom?.roomProfile?.roomProfile?.id ?? "",
index,
eventType: eventType,
inviterId: inviterId,
);
final targetIndex = micGoUpRes.micIndex ?? index;
final 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) {}
_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");
}
}
///自己是否在麦上
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 {
final value = await SCChatRoomRepository().specific(roomId);
final currentRoomProfile = currenRoom?.roomProfile?.roomProfile;
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,
),
),
),
);
///如果是游客禁止上麦,已经在麦上的游客需要下麦
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;
}
void updateRoomMusicPublishingState({
required String? userId,
required bool publishing,
bool notifyIfChanged = true,
}) {
final normalizedUserId = (userId ?? "").trim();
if (normalizedUserId.isEmpty) {
return;
}
bool changed = false;
if (publishing) {
changed = _roomMusicPublishingUserIds.add(normalizedUserId);
_roomMusicPublishingClearTimers.remove(normalizedUserId)?.cancel();
_roomMusicPublishingClearTimers[normalizedUserId] = Timer(
_roomMusicPublishingStateTtl,
() {
_roomMusicPublishingClearTimers.remove(normalizedUserId);
if (_roomMusicPublishingUserIds.remove(normalizedUserId)) {
notifyListeners();
}
},
);
} else {
_roomMusicPublishingClearTimers.remove(normalizedUserId)?.cancel();
changed = _roomMusicPublishingUserIds.remove(normalizedUserId);
}
if (changed && notifyIfChanged) {
notifyListeners();
}
}
void _clearRoomMusicPublishingStates() {
for (final timer in _roomMusicPublishingClearTimers.values) {
timer.cancel();
}
_roomMusicPublishingClearTimers.clear();
_roomMusicPublishingUserIds.clear();
}
void handleRoomMusicPublishingMessage(Msg msg) {
final userId =
(msg.role ?? "").trim().isNotEmpty ? msg.role : msg.user?.id ?? "";
final statusText = (msg.msg ?? "").trim().toLowerCase();
final publishing =
statusText == "1" || statusText == "true" || (msg.number ?? 0) > 0;
updateRoomMusicPublishingState(userId: userId, publishing: publishing);
}
void publishCurrentUserRoomMusicState(bool publishing) {
final currentUser = AccountStorage().getCurrentUser()?.userProfile;
final currentUserId = (currentUser?.id ?? "").trim();
if (currentUserId.isEmpty) {
return;
}
final effectivePublishing = publishing && isOnMai();
updateRoomMusicPublishingState(
userId: currentUserId,
publishing: effectivePublishing,
);
final groupId =
currenRoom?.roomProfile?.roomProfile?.roomAccount?.trim() ?? "";
if (groupId.isEmpty) {
return;
}
if (rtmProvider == null && context != null && context!.mounted) {
rtmProvider = Provider.of<RtmProvider>(context!, listen: false);
}
final roomRtmProvider = rtmProvider;
unawaited(
roomRtmProvider?.dispatchMessage(
Msg(
groupId: groupId,
msg: effectivePublishing ? "1" : "0",
type: SCRoomMsgType.roomMusic,
role: currentUserId,
number: effectivePublishing ? 1 : 0,
user: currentUser,
),
addLocal: false,
) ??
Future<void>.value(),
);
}
void republishCurrentUserRoomMusicStateIfNeeded() {
final currentUserId =
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
if (isUserPublishingRoomMusic(currentUserId)) {
publishCurrentUserRoomMusicState(true);
}
}
Future<void> startRoomMusicAudioMixing({
required String filePath,
required bool loopback,
int cycle = 1,
int startPos = 0,
}) async {
if (filePath.trim().isEmpty) {
return;
}
if (!loopback) {
final canPublish = await _ensureRoomMusicPublishReady();
if (!canPublish) {
throw StateError('Current user is not ready to publish room music');
}
}
await 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);
}
Future<bool> _ensureRoomMusicPublishReady() async {
if (!_agoraJoined || engine == null) {
return false;
}
final currentUserMic = _currentUserMicSeat();
if (currentUserMic == null) {
return false;
}
return _switchAgoraToBroadcaster(
publisherToken: currentUserMic.roomToken ?? "",
muted: (currentUserMic.micMute ?? false) || isMic,
);
}
}