1918 lines
59 KiB
Dart
1918 lines
59 KiB
Dart
import 'dart:async';
|
||
|
||
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
|
||
import 'package:fluro/fluro.dart';
|
||
import 'package:flutter/cupertino.dart';
|
||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||
import 'package:yumi/app_localizations.dart';
|
||
import 'package:yumi/app/constants/sc_room_msg_type.dart';
|
||
import 'package:yumi/shared/tools/sc_permission_utils.dart';
|
||
import 'package:yumi/shared/tools/sc_room_utils.dart';
|
||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
|
||
import 'package:yumi/services/room/rc_room_manager.dart';
|
||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
|
||
import 'package:provider/provider.dart';
|
||
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||
import 'package:yumi/app/routes/sc_routes.dart';
|
||
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
|
||
import 'package:yumi/shared/tools/sc_lk_dialog_util.dart';
|
||
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
||
import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart';
|
||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
||
import 'package:yumi/shared/business_logic/models/res/join_room_res.dart';
|
||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||
import 'package:yumi/shared/business_logic/models/res/mic_res.dart';
|
||
import 'package:yumi/shared/business_logic/models/res/room_res.dart';
|
||
import 'package:yumi/modules/room/voice_room_route.dart';
|
||
import 'package:yumi/ui_kit/widgets/room/empty_mai_select.dart';
|
||
import 'package:yumi/ui_kit/widgets/room/room_user_info_card.dart';
|
||
import '../../shared/tools/sc_heartbeat_utils.dart';
|
||
import '../../shared/data_sources/models/enum/sc_heartbeat_status.dart';
|
||
import '../../shared/data_sources/models/enum/sc_room_info_event_type.dart';
|
||
import '../../shared/data_sources/models/enum/sc_room_roles_type.dart';
|
||
import '../../shared/business_logic/models/res/sc_is_follow_room_res.dart';
|
||
import '../../shared/business_logic/models/res/sc_room_red_packet_list_res.dart';
|
||
import '../../shared/business_logic/models/res/sc_room_rocket_status_res.dart';
|
||
import '../../shared/business_logic/models/res/sc_room_theme_list_res.dart';
|
||
import '../../shared/tools/sc_room_profile_cache.dart';
|
||
import '../../ui_kit/components/sc_float_ichart.dart';
|
||
|
||
typedef OnSoundVoiceChange = Function(num index, int volum);
|
||
typedef RtcProvider = RealTimeCommunicationManager;
|
||
|
||
class RealTimeCommunicationManager extends ChangeNotifier {
|
||
static const Duration _micListPollingInterval = Duration(seconds: 2);
|
||
static const Duration _onlineUsersPollingInterval = Duration(seconds: 3);
|
||
static const Duration _selfMicStateGracePeriod = Duration(seconds: 4);
|
||
static const Duration _giftTriggeredMicRefreshMinInterval = Duration(
|
||
milliseconds: 900,
|
||
);
|
||
|
||
bool needUpDataUserInfo = false;
|
||
bool _roomVisualEffectsEnabled = false;
|
||
bool _isExitingCurrentVoiceRoomSession = false;
|
||
Timer? _micListPollingTimer;
|
||
Timer? _onlineUsersPollingTimer;
|
||
bool _isRefreshingMicList = false;
|
||
bool _isRefreshingOnlineUsers = false;
|
||
bool _pendingMicListRefresh = false;
|
||
Timer? _deferredMicListRefreshTimer;
|
||
int _lastMicListRefreshStartedAtMs = 0;
|
||
num? _preferredSelfMicIndex;
|
||
num? _pendingSelfMicSourceIndex;
|
||
int? _pendingSelfMicSwitchGuardUntilMs;
|
||
num? _pendingSelfMicReleaseIndex;
|
||
int? _pendingSelfMicReleaseGuardUntilMs;
|
||
ClientRoleType? _lastAppliedClientRole;
|
||
bool? _lastAppliedLocalAudioMuted;
|
||
bool? _lastScheduledVoiceLiveOnMic;
|
||
String? _lastScheduledVoiceLiveRoomId;
|
||
String? _lastScheduledAnchorRoomId;
|
||
|
||
///当前所在房间
|
||
JoinRoomRes? currenRoom;
|
||
|
||
/// 声音音量变化监听
|
||
final List<OnSoundVoiceChange> _onSoundVoiceChangeList = [];
|
||
|
||
///麦位
|
||
Map<num, MicRes> roomWheatMap = {};
|
||
|
||
RtcEngine? engine;
|
||
BuildContext? context;
|
||
RtmProvider? rtmProvider;
|
||
|
||
SCIsFollowRoomRes? isFollowRoomRes;
|
||
SCRoomRocketStatusRes? roomRocketStatus;
|
||
|
||
///房间是否静音
|
||
bool roomIsMute = false;
|
||
|
||
bool closeFullGame = false;
|
||
|
||
///禁音开关 默认关闭
|
||
bool isMic = true;
|
||
|
||
///在线用户列表
|
||
List<SocialChatUserProfile> onlineUsers = [];
|
||
|
||
///房间管理员
|
||
List<SocialChatUserProfile> managerUsers = [];
|
||
|
||
///音乐是否正在播放
|
||
bool isMusicPlaying = false;
|
||
|
||
///房间红包列表
|
||
List<SCRoomRedPacketListRes> redPacketList = [];
|
||
|
||
num roomTaskClaimableCount = 0;
|
||
|
||
initializeRealTimeCommunicationManager(BuildContext context) {
|
||
this.context = context;
|
||
}
|
||
|
||
bool get roomVisualEffectsEnabled => _roomVisualEffectsEnabled;
|
||
|
||
bool get isExitingCurrentVoiceRoomSession =>
|
||
_isExitingCurrentVoiceRoomSession;
|
||
|
||
bool get shouldShowRoomVisualEffects =>
|
||
currenRoom != null && _roomVisualEffectsEnabled;
|
||
|
||
void setRoomVisualEffectsEnabled(bool enabled) {
|
||
if (_roomVisualEffectsEnabled == enabled) {
|
||
return;
|
||
}
|
||
_roomVisualEffectsEnabled = enabled;
|
||
notifyListeners();
|
||
}
|
||
|
||
void _setExitingCurrentVoiceRoomSession(bool enabled, {bool notify = true}) {
|
||
if (_isExitingCurrentVoiceRoomSession == enabled) {
|
||
return;
|
||
}
|
||
_isExitingCurrentVoiceRoomSession = enabled;
|
||
if (notify) {
|
||
notifyListeners();
|
||
}
|
||
}
|
||
|
||
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 _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.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 normalizedUserId = (mergedMic.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] = mergedMic;
|
||
}
|
||
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);
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
void _syncSelfMicRuntimeState() {
|
||
final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id;
|
||
if ((currentUserId ?? "").isEmpty) {
|
||
return;
|
||
}
|
||
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||
|
||
MicRes? currentUserMic;
|
||
for (final mic in roomWheatMap.values) {
|
||
if (mic.user?.id == currentUserId) {
|
||
currentUserMic = mic;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (currentUserMic == null) {
|
||
_clearSelfMicSwitchGuard(clearPreferredIndex: true);
|
||
_syncRoomHeartbeatState(
|
||
roomId: roomId,
|
||
isOnMic: false,
|
||
shouldRunAnchorHeartbeat: false,
|
||
);
|
||
_applyLocalAudioRuntimeState(
|
||
clientRole: ClientRoleType.clientRoleAudience,
|
||
muted: true,
|
||
);
|
||
return;
|
||
}
|
||
|
||
_preferredSelfMicIndex = currentUserMic.micIndex;
|
||
_syncRoomHeartbeatState(
|
||
roomId: roomId,
|
||
isOnMic: true,
|
||
shouldRunAnchorHeartbeat: true,
|
||
);
|
||
|
||
if ((currentUserMic.micMute ?? false) || isMic) {
|
||
_applyLocalAudioRuntimeState(
|
||
clientRole: ClientRoleType.clientRoleAudience,
|
||
muted: true,
|
||
);
|
||
return;
|
||
}
|
||
|
||
_applyLocalAudioRuntimeState(
|
||
clientRole: ClientRoleType.clientRoleBroadcaster,
|
||
muted: false,
|
||
);
|
||
}
|
||
|
||
void _syncRoomHeartbeatState({
|
||
required String roomId,
|
||
required bool isOnMic,
|
||
required bool shouldRunAnchorHeartbeat,
|
||
}) {
|
||
final normalizedRoomId = roomId.trim();
|
||
if (normalizedRoomId.isEmpty) {
|
||
_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;
|
||
}
|
||
}
|
||
|
||
void _applyLocalAudioRuntimeState({
|
||
required ClientRoleType clientRole,
|
||
required bool muted,
|
||
}) {
|
||
if (engine == null) {
|
||
_lastAppliedClientRole = null;
|
||
_lastAppliedLocalAudioMuted = null;
|
||
return;
|
||
}
|
||
|
||
if (_lastAppliedClientRole != clientRole) {
|
||
engine?.setClientRole(role: clientRole);
|
||
_lastAppliedClientRole = clientRole;
|
||
}
|
||
if (!muted && _lastAppliedLocalAudioMuted != false) {
|
||
adjustRecordingSignalVolume(100);
|
||
}
|
||
if (_lastAppliedLocalAudioMuted != muted) {
|
||
engine?.muteLocalAudioStream(muted);
|
||
_lastAppliedLocalAudioMuted = muted;
|
||
}
|
||
}
|
||
|
||
void _resetHeartbeatTracking() {
|
||
_lastScheduledVoiceLiveOnMic = null;
|
||
_lastScheduledVoiceLiveRoomId = null;
|
||
_lastScheduledAnchorRoomId = null;
|
||
}
|
||
|
||
void _resetLocalAudioRuntimeTracking() {
|
||
_lastAppliedClientRole = null;
|
||
_lastAppliedLocalAudioMuted = null;
|
||
}
|
||
|
||
void _applyMicSnapshot(
|
||
Map<num, MicRes> nextMap, {
|
||
bool notifyIfUnchanged = true,
|
||
}) {
|
||
final changed = !_sameMicMaps(roomWheatMap, nextMap);
|
||
roomWheatMap = nextMap;
|
||
_syncSelfMicRuntimeState();
|
||
if (changed || notifyIfUnchanged) {
|
||
notifyListeners();
|
||
}
|
||
}
|
||
|
||
int? _tryParseAgoraUid(String? value) {
|
||
final normalizedValue = (value ?? '').trim();
|
||
if (normalizedValue.isEmpty) {
|
||
return null;
|
||
}
|
||
return int.tryParse(normalizedValue);
|
||
}
|
||
|
||
int _stableAgoraUidFromUser(SocialChatUserProfile? user) {
|
||
final source =
|
||
'${user?.account ?? ""}|${user?.id ?? ""}|${user?.userNickname ?? ""}';
|
||
var hash = 0x811C9DC5;
|
||
for (final codeUnit in source.codeUnits) {
|
||
hash ^= codeUnit;
|
||
hash = (hash * 0x01000193) & 0x7fffffff;
|
||
}
|
||
return hash == 0 ? 1 : hash;
|
||
}
|
||
|
||
int _resolveAgoraUidForUser(SocialChatUserProfile? user) {
|
||
final parsedAccountUid = _tryParseAgoraUid(user?.account);
|
||
if (parsedAccountUid != null && parsedAccountUid > 0) {
|
||
return parsedAccountUid;
|
||
}
|
||
final parsedUserIdUid = _tryParseAgoraUid(user?.id);
|
||
if (parsedUserIdUid != null && parsedUserIdUid > 0) {
|
||
return parsedUserIdUid;
|
||
}
|
||
return _stableAgoraUidFromUser(user);
|
||
}
|
||
|
||
int _resolveAgoraUidForCurrentUser() {
|
||
return _resolveAgoraUidForUser(
|
||
AccountStorage().getCurrentUser()?.userProfile,
|
||
);
|
||
}
|
||
|
||
void requestGiftTriggeredMicRefresh() {
|
||
requestMicrophoneListRefresh(
|
||
notifyIfUnchanged: false,
|
||
minInterval: _giftTriggeredMicRefreshMinInterval,
|
||
);
|
||
}
|
||
|
||
void requestMicrophoneListRefresh({
|
||
bool notifyIfUnchanged = false,
|
||
Duration minInterval = Duration.zero,
|
||
}) {
|
||
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||
if (roomId.isEmpty) {
|
||
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> joinAgoraVoiceChannel() async {
|
||
try {
|
||
engine = await _initAgoraRtcEngine();
|
||
engine?.setAudioProfile(
|
||
profile: AudioProfileType.audioProfileSpeechStandard,
|
||
scenario: AudioScenarioType.audioScenarioGameStreaming,
|
||
);
|
||
engine?.enableAudioVolumeIndication(
|
||
interval: 500,
|
||
smooth: 3,
|
||
reportVad: true,
|
||
);
|
||
await engine?.disableVideo();
|
||
await engine?.setLocalPublishFallbackOption(
|
||
StreamFallbackOptions.streamFallbackOptionAudioOnly,
|
||
);
|
||
engine?.registerEventHandler(
|
||
RtcEngineEventHandler(
|
||
onError: (ErrorCodeType err, String msg) {
|
||
print('rtc错误${err}');
|
||
},
|
||
onAudioMixingStateChanged: (
|
||
AudioMixingStateType state,
|
||
AudioMixingReasonType reason,
|
||
) {
|
||
switch (state) {
|
||
case AudioMixingStateType.audioMixingStatePlaying:
|
||
isMusicPlaying = true;
|
||
break;
|
||
case AudioMixingStateType.audioMixingStateStopped:
|
||
case AudioMixingStateType.audioMixingStateFailed:
|
||
isMusicPlaying = false;
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
},
|
||
onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
|
||
print('rtc 自己加入 ${connection.channelId} ${connection.localUid}');
|
||
},
|
||
onUserJoined: (connection, remoteUid, elapsed) {
|
||
print('rtc用户 $remoteUid 加入了频道');
|
||
},
|
||
// 监听远端用户离开
|
||
onUserOffline: (connection, remoteUid, reason) {
|
||
print('rtc用户 $remoteUid 离开了频道 (原因: ${reason})');
|
||
},
|
||
onTokenPrivilegeWillExpire: (
|
||
RtcConnection connection,
|
||
String token,
|
||
) async {
|
||
var rtcToken = await SCAccountRepository().getRtcToken(
|
||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||
AccountStorage().getCurrentUser()?.userProfile?.id ?? "",
|
||
isPublisher: isOnMai(),
|
||
);
|
||
engine?.renewToken(rtcToken.rtcToken ?? "");
|
||
},
|
||
onAudioVolumeIndication: initializeAudioVolumeIndicationCallback,
|
||
),
|
||
);
|
||
var rtcToken = await SCAccountRepository().getRtcToken(
|
||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||
AccountStorage().getCurrentUser()?.userProfile?.id ?? "",
|
||
isPublisher: false,
|
||
);
|
||
await engine?.joinChannel(
|
||
token: rtcToken.rtcToken ?? "",
|
||
channelId: currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||
uid: _resolveAgoraUidForCurrentUser(),
|
||
options: ChannelMediaOptions(
|
||
// 自动订阅所有视频流
|
||
autoSubscribeVideo: false,
|
||
// 自动订阅所有音频流
|
||
autoSubscribeAudio: true,
|
||
// 发布摄像头采集的视频
|
||
publishCameraTrack: false,
|
||
// 发布麦克风采集的音频
|
||
publishMicrophoneTrack: true,
|
||
// 设置用户角色为 clientRoleBroadcaster(主播)或 clientRoleAudience(观众)
|
||
clientRoleType: ClientRoleType.clientRoleAudience,
|
||
channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
|
||
),
|
||
);
|
||
_syncSelfMicRuntimeState();
|
||
engine?.muteAllRemoteAudioStreams(roomIsMute);
|
||
} catch (e) {
|
||
SCTts.show("Join room fail");
|
||
exitCurrentVoiceRoomSession(false);
|
||
print('加入失败:${e.runtimeType},${e.toString()}');
|
||
}
|
||
}
|
||
|
||
Future<RtcEngine> _initAgoraRtcEngine() async {
|
||
RtcEngine? engine;
|
||
while (engine == null) {
|
||
engine = createAgoraRtcEngine();
|
||
await engine.initialize(
|
||
RtcEngineContext(appId: SCGlobalConfig.agoraRtcAppid),
|
||
);
|
||
}
|
||
return engine;
|
||
}
|
||
|
||
void initializeAudioVolumeIndicationCallback(
|
||
RtcConnection connection,
|
||
List<AudioVolumeInfo> speakers,
|
||
int speakerNumber,
|
||
int totalVolume,
|
||
) {
|
||
// if (user == null) {
|
||
// throw "本地User尚未初始化";
|
||
// }
|
||
// print('初始化声音监听器:${user.toJson()}');
|
||
|
||
if (roomWheatMap.isEmpty || speakers.isEmpty) {
|
||
return;
|
||
}
|
||
|
||
for (final seat in roomWheatMap.values) {
|
||
seat.setVolume = 0;
|
||
}
|
||
|
||
final seatIndexByAgoraUid = <int, num>{};
|
||
roomWheatMap.forEach((seatIndex, mic) {
|
||
final user = mic.user;
|
||
if (user == null) {
|
||
return;
|
||
}
|
||
seatIndexByAgoraUid[_resolveAgoraUidForUser(user)] = seatIndex;
|
||
});
|
||
|
||
final currentUserAgoraUid = _resolveAgoraUidForCurrentUser();
|
||
final listeners = List<OnSoundVoiceChange>.from(_onSoundVoiceChangeList);
|
||
for (final info in speakers) {
|
||
final volume = info.volume ?? 0;
|
||
if (volume <= 0) {
|
||
continue;
|
||
}
|
||
|
||
final resolvedAgoraUid = info.uid == 0 ? currentUserAgoraUid : info.uid;
|
||
final seatIndex = seatIndexByAgoraUid[resolvedAgoraUid];
|
||
if (seatIndex == null) {
|
||
continue;
|
||
}
|
||
|
||
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? 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),
|
||
roomName: _preferNonEmpty(roomName, currentRoomProfile.roomName),
|
||
roomDesc: _preferNonEmpty(roomDesc, currentRoomProfile.roomDesc),
|
||
),
|
||
),
|
||
);
|
||
SCRoomProfileCache.saveRoomProfile(
|
||
roomId: currentRoomProfile.id ?? "",
|
||
roomCover: roomCover,
|
||
roomName: roomName,
|
||
roomDesc: roomDesc,
|
||
);
|
||
notifyListeners();
|
||
}
|
||
|
||
void joinVoiceRoomSession(
|
||
BuildContext context,
|
||
String roomId, {
|
||
String? pwd,
|
||
bool clearRoomData = false,
|
||
bool needOpenRedenvelope = false,
|
||
String redPackId = "",
|
||
}) async {
|
||
int nextTime = DateTime.now().millisecondsSinceEpoch;
|
||
if (nextTime - startTime < 1000) {
|
||
//频繁点击
|
||
return;
|
||
}
|
||
startTime = nextTime;
|
||
if (clearRoomData) {
|
||
_clearData();
|
||
}
|
||
bool hasPermission = await SCPermissionUtils.checkMicrophonePermission();
|
||
if (!hasPermission) {
|
||
SCTts.show('Microphone permission is denied.');
|
||
throw ArgumentError('Microphone permission is denied.');
|
||
}
|
||
if (roomId == currenRoom?.roomProfile?.roomProfile?.id) {
|
||
///最小化进入房间,或者进入的是同一个房间
|
||
final loaded = await loadRoomInfo(
|
||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||
);
|
||
if (!loaded) {
|
||
return;
|
||
}
|
||
if (!context.mounted) {
|
||
return;
|
||
}
|
||
Provider.of<SocialChatUserProfileManager>(
|
||
context,
|
||
listen: false,
|
||
).fetchUserProfileData();
|
||
retrieveMicrophoneList();
|
||
fetchOnlineUsersList();
|
||
_startRoomStatePolling();
|
||
setRoomVisualEffectsEnabled(true);
|
||
SCFloatIchart().remove();
|
||
SCNavigatorUtils.push(
|
||
context,
|
||
'${VoiceRoomRoute.voiceRoom}?id=$roomId',
|
||
transition: TransitionType.custom,
|
||
transitionDuration: kOpenRoomTransitionDuration,
|
||
transitionBuilder: (
|
||
BuildContext context,
|
||
Animation<double> animation,
|
||
Animation<double> secondaryAnimation,
|
||
Widget child,
|
||
) {
|
||
return kOpenRoomTransitionBuilder(
|
||
context,
|
||
animation,
|
||
secondaryAnimation,
|
||
child,
|
||
);
|
||
},
|
||
);
|
||
} else {
|
||
SCFloatIchart().remove();
|
||
if (currenRoom != null) {
|
||
await exitCurrentVoiceRoomSession(false);
|
||
}
|
||
if (!context.mounted) {
|
||
return;
|
||
}
|
||
rtmProvider = Provider.of<RtmProvider>(context, listen: false);
|
||
SCLoadingManager.show(context: context);
|
||
try {
|
||
currenRoom = await SCAccountRepository().entryRoom(
|
||
roomId,
|
||
pwd: pwd,
|
||
redPackId: redPackId,
|
||
needOpenRedenvelope: needOpenRedenvelope,
|
||
);
|
||
await initializeRoomSession(
|
||
needOpenRedenvelope: needOpenRedenvelope,
|
||
redPackId: redPackId,
|
||
);
|
||
notifyListeners();
|
||
} catch (e) {
|
||
await resetLocalRoomState(fallbackRtmProvider: rtmProvider);
|
||
rethrow;
|
||
} finally {
|
||
SCLoadingManager.hide();
|
||
}
|
||
}
|
||
}
|
||
|
||
///初始化房间相关
|
||
Future initializeRoomSession({
|
||
bool needOpenRedenvelope = false,
|
||
String? redPackId,
|
||
}) async {
|
||
if ((currenRoom?.roomProfile?.roomProfile?.event ==
|
||
SCRoomInfoEventType.WAITING_CONFIRMED.name ||
|
||
currenRoom?.roomProfile?.roomProfile?.event ==
|
||
SCRoomInfoEventType.ID_CHANGE.name) &&
|
||
currenRoom?.entrants?.roles == SCRoomRolesType.HOMEOWNER.name) {
|
||
///需要去修改房间信息,创建群聊
|
||
SCNavigatorUtils.push(
|
||
context!,
|
||
"${VoiceRoomRoute.roomEdit}?need=true",
|
||
replace: false,
|
||
);
|
||
return Future;
|
||
}
|
||
SCRoomUtils.roomSCGlobalConfig(
|
||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||
);
|
||
if (currenRoom?.roomProfile?.roomProfile?.id !=
|
||
AccountStorage().getCurrentUser()?.userProfile?.id) {
|
||
SCChatRoomRepository()
|
||
.isFollowRoom(currenRoom?.roomProfile?.roomProfile?.id ?? "")
|
||
.then((value) {
|
||
isFollowRoomRes = value;
|
||
notifyListeners();
|
||
});
|
||
}
|
||
setRoomVisualEffectsEnabled(true);
|
||
SCNavigatorUtils.push(context!, VoiceRoomRoute.voiceRoom, replace: false);
|
||
var joinResult = await rtmProvider?.joinRoomGroup(
|
||
currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "",
|
||
"",
|
||
);
|
||
SCChatRoomRepository()
|
||
.rocketClaim(currenRoom?.roomProfile?.roomProfile?.id ?? "")
|
||
.catchError((e) {
|
||
return true; // 错误已处理
|
||
});
|
||
rtmProvider?.addMsg(
|
||
Msg(
|
||
groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "",
|
||
msg: SCAppLocalizations.of(context!)!.systemRoomTips,
|
||
type: SCRoomMsgType.systemTips,
|
||
),
|
||
);
|
||
rtmProvider?.addMsg(
|
||
Msg(
|
||
groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "",
|
||
msg: currenRoom?.roomProfile?.roomProfile?.roomDesc,
|
||
type: SCRoomMsgType.systemTips,
|
||
),
|
||
);
|
||
|
||
///获取麦位
|
||
retrieveMicrophoneList();
|
||
fetchOnlineUsersList();
|
||
_startRoomStatePolling();
|
||
Provider.of<SocialChatRoomManager>(
|
||
context!,
|
||
listen: false,
|
||
).fetchContributionLevelData(
|
||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||
);
|
||
await joinAgoraVoiceChannel();
|
||
if (joinResult?.code == 0) {
|
||
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);
|
||
if (SCGlobalConfig.isEntryVehicleAnimation) {
|
||
if (AccountStorage().getCurrentUser()?.userProfile?.getMountains() !=
|
||
null &&
|
||
shouldShowRoomVisualEffects) {
|
||
Future.delayed(Duration(milliseconds: 550), () {
|
||
SCGiftVapSvgaManager().play(
|
||
AccountStorage()
|
||
.getCurrentUser()
|
||
?.userProfile
|
||
?.getMountains()
|
||
?.sourceUrl ??
|
||
"",
|
||
priority: 100,
|
||
type: 1,
|
||
);
|
||
});
|
||
}
|
||
}
|
||
if (rtmProvider?.msgUserJoinListener != null) {
|
||
rtmProvider?.msgUserJoinListener!(joinMsg);
|
||
}
|
||
}
|
||
SCChatRoomRepository()
|
||
.rocketStatus(currenRoom?.roomProfile?.roomProfile?.id ?? "")
|
||
.then((res) {
|
||
roomRocketStatus = res;
|
||
notifyListeners();
|
||
})
|
||
.catchError((e) {});
|
||
loadRoomRedPacketList(1);
|
||
|
||
fetchRoomTaskClaimableCount();
|
||
}
|
||
|
||
///更新房间火箭信息
|
||
void updateRoomRocketConfigurationStatus(SCRoomRocketStatusRes res) {
|
||
roomRocketStatus = res;
|
||
notifyListeners();
|
||
}
|
||
|
||
///获取在线用户
|
||
Future<void> fetchOnlineUsersList({bool notifyIfUnchanged = true}) async {
|
||
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||
if (roomId.isEmpty || _isRefreshingOnlineUsers) {
|
||
return;
|
||
}
|
||
_isRefreshingOnlineUsers = true;
|
||
try {
|
||
final fetchedUsers = await SCChatRoomRepository().roomOnlineUsers(roomId);
|
||
if (roomId != currenRoom?.roomProfile?.roomProfile?.id) {
|
||
return;
|
||
}
|
||
final changed = !_sameOnlineUsers(onlineUsers, fetchedUsers);
|
||
onlineUsers = fetchedUsers;
|
||
_refreshManagerUsers(onlineUsers);
|
||
if (changed || notifyIfUnchanged) {
|
||
notifyListeners();
|
||
}
|
||
} finally {
|
||
_isRefreshingOnlineUsers = false;
|
||
}
|
||
}
|
||
|
||
Future<void> retrieveMicrophoneList({bool notifyIfUnchanged = true}) async {
|
||
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||
if (roomId.isEmpty) {
|
||
return;
|
||
}
|
||
if (_isRefreshingMicList) {
|
||
_pendingMicListRefresh = true;
|
||
return;
|
||
}
|
||
_isRefreshingMicList = true;
|
||
_lastMicListRefreshStartedAtMs = DateTime.now().millisecondsSinceEpoch;
|
||
try {
|
||
final roomWheatList = await SCChatRoomRepository().micList(roomId);
|
||
if (roomId != currenRoom?.roomProfile?.roomProfile?.id) {
|
||
return;
|
||
}
|
||
final nextMap = _buildMicMap(roomWheatList);
|
||
_applyMicSnapshot(nextMap, notifyIfUnchanged: notifyIfUnchanged);
|
||
} finally {
|
||
_isRefreshingMicList = false;
|
||
if (_pendingMicListRefresh) {
|
||
_pendingMicListRefresh = false;
|
||
unawaited(
|
||
Future<void>.microtask(
|
||
() => retrieveMicrophoneList(notifyIfUnchanged: notifyIfUnchanged),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
void fetchRoomTaskClaimableCount() {
|
||
SCChatRoomRepository()
|
||
.roomTaskClaimableCount()
|
||
.then((res) {
|
||
roomTaskClaimableCount = res.claimableCount ?? 0;
|
||
notifyListeners();
|
||
})
|
||
.catchError((e) {});
|
||
}
|
||
|
||
Future<List<SCRoomRedPacketListRes>> loadRoomRedPacketList(
|
||
int current,
|
||
) async {
|
||
var result = await SCChatRoomRepository().roomRedPacketList(
|
||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||
current,
|
||
);
|
||
if (current == 1) {
|
||
redPacketList = result;
|
||
notifyListeners();
|
||
}
|
||
return result;
|
||
}
|
||
|
||
Future exitCurrentVoiceRoomSession(bool isLogout) async {
|
||
_setExitingCurrentVoiceRoomSession(true);
|
||
_stopRoomStatePolling();
|
||
try {
|
||
rtmProvider?.msgAllListener = null;
|
||
rtmProvider?.msgChatListener = null;
|
||
rtmProvider?.msgGiftListener = null;
|
||
SCLoadingManager.show(context: context);
|
||
SCGiftVapSvgaManager().stopPlayback();
|
||
setRoomVisualEffectsEnabled(false);
|
||
SCHeartbeatUtils.scheduleHeartbeat(SCHeartbeatStatus.ONLINE.name, false);
|
||
SCHeartbeatUtils.cancelAnchorTimer();
|
||
rtmProvider?.cleanRoomData();
|
||
await engine?.leaveChannel();
|
||
await Future.delayed(Duration(milliseconds: 100));
|
||
await engine?.release();
|
||
await rtmProvider?.quitGroup(
|
||
currenRoom!.roomProfile?.roomProfile?.roomAccount ?? "",
|
||
);
|
||
await SCAccountRepository().quitRoom(
|
||
currenRoom!.roomProfile?.roomProfile?.id ?? "",
|
||
);
|
||
_clearData();
|
||
SCLoadingManager.hide();
|
||
if (!isLogout) {
|
||
SCNavigatorUtils.popUntil(context!, ModalRoute.withName(SCRoutes.home));
|
||
}
|
||
} catch (e) {
|
||
print('rtc退出房间出错: $e');
|
||
_clearData();
|
||
SCLoadingManager.hide();
|
||
if (!isLogout) {
|
||
SCNavigatorUtils.popUntil(context!, ModalRoute.withName(SCRoutes.home));
|
||
}
|
||
}
|
||
}
|
||
|
||
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 engine?.leaveChannel();
|
||
await Future.delayed(const Duration(milliseconds: 100));
|
||
await engine?.release();
|
||
} catch (e) {
|
||
print('rtc清理本地房间状态出错: $e');
|
||
} finally {
|
||
engine = null;
|
||
rtmProvider?.cleanRoomData();
|
||
_clearData();
|
||
notifyListeners();
|
||
}
|
||
}
|
||
|
||
///清空列表数据
|
||
void _clearData() {
|
||
_stopRoomStatePolling();
|
||
engine = null;
|
||
_resetHeartbeatTracking();
|
||
_resetLocalAudioRuntimeTracking();
|
||
roomRocketStatus = null;
|
||
rtmProvider
|
||
?.onNewMessageListenerGroupMap["${currenRoom?.roomProfile?.roomProfile?.roomAccount}"] =
|
||
null;
|
||
roomWheatMap.clear();
|
||
onlineUsers.clear();
|
||
managerUsers.clear();
|
||
needUpDataUserInfo = false;
|
||
SCRoomUtils.roomUsersMap.clear();
|
||
_clearSelfMicSwitchGuard(clearPreferredIndex: true);
|
||
_clearSelfMicReleaseGuard();
|
||
roomIsMute = false;
|
||
rtmProvider?.roomAllMsgList.clear();
|
||
rtmProvider?.roomChatMsgList.clear();
|
||
redPacketList.clear();
|
||
currenRoom = null;
|
||
_roomVisualEffectsEnabled = false;
|
||
_isExitingCurrentVoiceRoomSession = false;
|
||
isMic = true;
|
||
isMusicPlaying = false;
|
||
DataPersistence.setLastTimeRoomId("");
|
||
SCGiftVapSvgaManager().stopPlayback();
|
||
SCRoomUtils.closeAllDialogs();
|
||
SCGlobalConfig.resetVisualEffectSwitchesToRecommendedDefaults();
|
||
}
|
||
|
||
void toggleRemoteAudioMuteForAllUsers() {
|
||
roomIsMute = !roomIsMute;
|
||
engine?.muteAllRemoteAudioStreams(roomIsMute);
|
||
notifyListeners();
|
||
}
|
||
|
||
///点击的位置
|
||
void clickSite(num index, {SocialChatUserProfile? clickUser}) {
|
||
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 = roomWheatMap[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 = roomWheatMap[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() {
|
||
retrieveMicrophoneList(notifyIfUnchanged: false).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);
|
||
print('添加监听:${_onSoundVoiceChangeList.length}');
|
||
}
|
||
|
||
removeSoundVoiceChangeListener(OnSoundVoiceChange onSoundVoiceChange) {
|
||
_onSoundVoiceChangeList.remove(onSoundVoiceChange);
|
||
print('删除监听:${_onSoundVoiceChangeList.length}');
|
||
}
|
||
|
||
void shangMai(num index, {String? eventType, String? inviterId}) async {
|
||
var myUser = AccountStorage().getCurrentUser()?.userProfile;
|
||
if (currenRoom?.roomProfile?.roomSetting?.touristMike ?? false) {
|
||
} else {
|
||
if (isTourists()) {
|
||
SCTts.show(
|
||
SCAppLocalizations.of(context!)!.touristsAreNotAllowedToGoOnTheMic,
|
||
);
|
||
return;
|
||
}
|
||
}
|
||
|
||
try {
|
||
var micGoUpRes = await SCChatRoomRepository().micGoUp(
|
||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||
index,
|
||
eventType: eventType,
|
||
inviterId: inviterId,
|
||
);
|
||
|
||
/// 设置成主播角色
|
||
engine?.renewToken(micGoUpRes.roomToken ?? "");
|
||
if (!micGoUpRes.micMute!) {
|
||
if (!isMic) {
|
||
engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
|
||
adjustRecordingSignalVolume(100);
|
||
await engine?.muteLocalAudioStream(false);
|
||
}
|
||
} else {
|
||
engine?.setClientRole(role: ClientRoleType.clientRoleAudience);
|
||
await engine?.muteLocalAudioStream(true);
|
||
}
|
||
SCHeartbeatUtils.scheduleAnchorHeartbeat(
|
||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||
);
|
||
final targetIndex = micGoUpRes.micIndex ?? index;
|
||
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,
|
||
);
|
||
}
|
||
if (roomWheatMap[targetIndex]?.micMute ?? false) {
|
||
///房主上麦自动解禁麦位
|
||
if (isFz()) {
|
||
await jieJinMai(targetIndex);
|
||
}
|
||
}
|
||
|
||
notifyListeners();
|
||
_refreshMicListSilently();
|
||
} catch (ex) {
|
||
SCTts.show('Failed to put on the microphone, $ex');
|
||
}
|
||
}
|
||
|
||
xiaMai(num index) async {
|
||
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;
|
||
engine?.muteLocalAudioStream(true);
|
||
}
|
||
_clearSelfMicSwitchGuard(clearPreferredIndex: true);
|
||
if (currentUserSeatIndex > -1) {
|
||
_startSelfMicReleaseGuard(sourceIndex: currentUserSeatIndex);
|
||
} else {
|
||
_clearSelfMicReleaseGuard();
|
||
}
|
||
SCHeartbeatUtils.cancelAnchorTimer();
|
||
|
||
/// 设置成主持人角色
|
||
engine?.renewToken("");
|
||
engine?.setClientRole(role: ClientRoleType.clientRoleAudience);
|
||
_clearUserFromSeats(currentUserId);
|
||
notifyListeners();
|
||
requestMicrophoneListRefresh(
|
||
notifyIfUnchanged: false,
|
||
minInterval: const Duration(milliseconds: 350),
|
||
);
|
||
} catch (ex) {
|
||
SCTts.show('Failed to leave the microphone, $ex');
|
||
}
|
||
}
|
||
|
||
///踢人下麦
|
||
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");
|
||
print('踢人下麦失败');
|
||
}
|
||
}
|
||
|
||
///自己是否在麦上
|
||
bool isOnMai() {
|
||
final currentUserId =
|
||
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
|
||
if (currentUserId.isEmpty) {
|
||
return false;
|
||
}
|
||
for (final entry in roomWheatMap.entries) {
|
||
if ((micAtIndexForDisplay(entry.key)?.user?.id ?? "").trim() ==
|
||
currentUserId) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
///自己是否在指定的麦上
|
||
bool isOnMaiInIndex(num index) {
|
||
return (micAtIndexForDisplay(index)?.user?.id ?? "").trim() ==
|
||
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
|
||
}
|
||
|
||
///点击的用户在哪个麦上
|
||
num userOnMaiInIndex(String userId) {
|
||
final normalizedUserId = userId.trim();
|
||
if (normalizedUserId.isEmpty) {
|
||
return -1;
|
||
}
|
||
num index = -1;
|
||
roomWheatMap.forEach((k, value) {
|
||
final visibleSeat =
|
||
normalizedUserId ==
|
||
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "")
|
||
.trim()
|
||
? micAtIndexForDisplay(k)
|
||
: value;
|
||
if ((visibleSeat?.user?.id ?? "").trim() == normalizedUserId) {
|
||
index = k;
|
||
}
|
||
});
|
||
return index;
|
||
}
|
||
|
||
///座位上的用户是否开麦
|
||
bool userOnMaiInIndexOfMute(String userId) {
|
||
bool micMute = false;
|
||
roomWheatMap.forEach((k, value) {
|
||
if (value.user?.id == userId) {
|
||
value.micMute;
|
||
}
|
||
});
|
||
return micMute;
|
||
}
|
||
|
||
///是否是房主
|
||
bool isFz() {
|
||
return currenRoom?.entrants?.roles == SCRoomRolesType.HOMEOWNER.name;
|
||
}
|
||
|
||
///是否是管理
|
||
bool isGL() {
|
||
return currenRoom?.entrants?.roles == SCRoomRolesType.ADMIN.name;
|
||
}
|
||
|
||
///是否是会员
|
||
bool isHY() {
|
||
return currenRoom?.entrants?.roles == SCRoomRolesType.MEMBER.name;
|
||
}
|
||
|
||
///是否是游客
|
||
bool isTourists() {
|
||
return currenRoom?.entrants?.roles == SCRoomRolesType.TOURIST.name;
|
||
}
|
||
|
||
///麦位变动
|
||
void micChange(List<MicRes>? mics) {
|
||
if (mics == null || mics.isEmpty) {
|
||
return;
|
||
}
|
||
_applyMicSnapshot(_buildMicMap(mics));
|
||
}
|
||
|
||
///找一个空麦位 -1表示没有空位
|
||
num findWheat() {
|
||
for (var entry in roomWheatMap.entries) {
|
||
if (entry.value.user == null && !entry.value.micLock!) {
|
||
return entry.key;
|
||
}
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
Future<bool> loadRoomInfo(String roomId) async {
|
||
try {
|
||
debugPrint("[Room Cover Sync] loadRoomInfo start roomId=$roomId");
|
||
final value = await SCChatRoomRepository().specific(roomId);
|
||
final currentRoomProfile = currenRoom?.roomProfile?.roomProfile;
|
||
debugPrint(
|
||
"[Room Cover Sync] loadRoomInfo fetched roomId=${value.id ?? roomId} roomCover=${value.roomCover ?? ""} roomName=${value.roomName ?? ""}",
|
||
);
|
||
currenRoom = currenRoom?.copyWith(
|
||
roomProfile: currenRoom?.roomProfile?.copyWith(
|
||
roomCounter: value.counter ?? currenRoom?.roomProfile?.roomCounter,
|
||
roomSetting: value.setting ?? currenRoom?.roomProfile?.roomSetting,
|
||
roomProfile: currentRoomProfile?.copyWith(
|
||
roomCover: _preferNonEmpty(
|
||
value.roomCover,
|
||
currentRoomProfile.roomCover,
|
||
),
|
||
roomName: _preferNonEmpty(
|
||
value.roomName,
|
||
currentRoomProfile.roomName,
|
||
),
|
||
roomDesc: _preferNonEmpty(
|
||
value.roomDesc,
|
||
currentRoomProfile.roomDesc,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
debugPrint(
|
||
"[Room Cover Sync] loadRoomInfo applied roomId=$roomId roomCover=${currenRoom?.roomProfile?.roomProfile?.roomCover ?? ""}",
|
||
);
|
||
|
||
///如果是游客禁止上麦,已经在麦上的游客需要下麦
|
||
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)) {
|
||
Provider.of<RtcProvider>(
|
||
context!,
|
||
listen: false,
|
||
).engine?.setClientRole(role: ClientRoleType.clientRoleAudience);
|
||
Provider.of<RtcProvider>(
|
||
context!,
|
||
listen: false,
|
||
).engine?.muteLocalAudioStream(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 (!Provider.of<RtcProvider>(context!, listen: false).isMic) {
|
||
if (roomWheatMap[index]?.user != null) {
|
||
adjustRecordingSignalVolume(100);
|
||
Provider.of<RtcProvider>(
|
||
context!,
|
||
listen: false,
|
||
).engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
|
||
Provider.of<RtcProvider>(
|
||
context!,
|
||
listen: false,
|
||
).engine?.muteLocalAudioStream(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;
|
||
}
|
||
bool isExtOnlineList = false;
|
||
for (var us in onlineUsers) {
|
||
if (us.id == user.id) {
|
||
isExtOnlineList = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!isExtOnlineList) {
|
||
Provider.of<RtcProvider>(context!, listen: false).onlineUsers.add(user);
|
||
_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) {
|
||
if (msg.number! > -1) {
|
||
var mic = roomWheatMap[msg.number];
|
||
if (mic != null && mic.user != null) {
|
||
roomWheatMap[msg.number!] = 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);
|
||
}
|
||
}
|