2026-04-21 13:44:03 +08:00

1918 lines
59 KiB
Dart
Raw Permalink 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: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);
}
}