修复自己刚上麦成功后,会立即触发一次 /live/mic/list 刷新。如果这次拉到的是旧麦位快照,本地 roomWheatMap 会把“自己在麦上”的状态清掉,isOnMai() 变成 false,底部栏麦克风就被隐藏了。之前只保护了 MIC_CHANGE 和换麦位场景,没有保护“自己初次上麦”的旧快照覆盖

This commit is contained in:
zhx 2026-05-26 16:36:07 +08:00
parent 10d5e821f7
commit 15eb93c810
2 changed files with 148 additions and 45 deletions

View File

@ -428,6 +428,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
final Map<num, _RecentMicChangeSeatSnapshot> _recentMicChangeSeatSnapshots = final Map<num, _RecentMicChangeSeatSnapshot> _recentMicChangeSeatSnapshots =
<num, _RecentMicChangeSeatSnapshot>{}; <num, _RecentMicChangeSeatSnapshot>{};
num? _preferredSelfMicIndex; num? _preferredSelfMicIndex;
num? _pendingSelfMicGoUpIndex;
int? _pendingSelfMicGoUpGuardUntilMs;
num? _pendingSelfMicSourceIndex; num? _pendingSelfMicSourceIndex;
int? _pendingSelfMicSwitchGuardUntilMs; int? _pendingSelfMicSwitchGuardUntilMs;
num? _pendingSelfMicReleaseIndex; num? _pendingSelfMicReleaseIndex;
@ -1218,6 +1220,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
required num sourceIndex, required num sourceIndex,
required num targetIndex, required num targetIndex,
}) { }) {
_clearSelfMicGoUpGuard();
_clearSelfMicReleaseGuard(); _clearSelfMicReleaseGuard();
_preferredSelfMicIndex = targetIndex; _preferredSelfMicIndex = targetIndex;
_pendingSelfMicSourceIndex = sourceIndex; _pendingSelfMicSourceIndex = sourceIndex;
@ -1225,6 +1228,22 @@ class RealTimeCommunicationManager extends ChangeNotifier {
DateTime.now().add(_selfMicStateGracePeriod).millisecondsSinceEpoch; DateTime.now().add(_selfMicStateGracePeriod).millisecondsSinceEpoch;
} }
void _startSelfMicGoUpGuard({required num targetIndex}) {
_clearSelfMicReleaseGuard();
_preferredSelfMicIndex = targetIndex;
_pendingSelfMicGoUpIndex = targetIndex;
_pendingSelfMicGoUpGuardUntilMs =
DateTime.now().add(_selfMicStateGracePeriod).millisecondsSinceEpoch;
}
void _clearSelfMicGoUpGuard({bool clearPreferredIndex = false}) {
_pendingSelfMicGoUpIndex = null;
_pendingSelfMicGoUpGuardUntilMs = null;
if (clearPreferredIndex) {
_preferredSelfMicIndex = null;
}
}
void _clearSelfMicSwitchGuard({bool clearPreferredIndex = false}) { void _clearSelfMicSwitchGuard({bool clearPreferredIndex = false}) {
_pendingSelfMicSourceIndex = null; _pendingSelfMicSourceIndex = null;
_pendingSelfMicSwitchGuardUntilMs = null; _pendingSelfMicSwitchGuardUntilMs = null;
@ -1233,6 +1252,11 @@ class RealTimeCommunicationManager extends ChangeNotifier {
} }
} }
@visibleForTesting
void debugStartSelfMicGoUpGuard(num targetIndex) {
_startSelfMicGoUpGuard(targetIndex: targetIndex);
}
void _startSelfMicReleaseGuard({required num sourceIndex}) { void _startSelfMicReleaseGuard({required num sourceIndex}) {
_pendingSelfMicReleaseIndex = sourceIndex; _pendingSelfMicReleaseIndex = sourceIndex;
_pendingSelfMicReleaseGuardUntilMs = _pendingSelfMicReleaseGuardUntilMs =
@ -1305,9 +1329,31 @@ class RealTimeCommunicationManager extends ChangeNotifier {
if (targetIndex != null && if (targetIndex != null &&
nextMap[targetIndex]?.user?.id == currentUserId) { nextMap[targetIndex]?.user?.id == currentUserId) {
_clearSelfMicSwitchGuard(); _clearSelfMicSwitchGuard();
_clearSelfMicGoUpGuard();
return nextMap; return nextMap;
} }
final goUpIndex = _pendingSelfMicGoUpIndex;
final goUpGuardUntilMs = _pendingSelfMicGoUpGuardUntilMs ?? 0;
final hasActiveGoUpGuard = goUpIndex != null && goUpGuardUntilMs > nowMs;
if (!hasActiveGoUpGuard) {
_clearSelfMicGoUpGuard();
} else if (selfSeatIndices.isEmpty ||
selfSeatIndices.every((seatIndex) => seatIndex == goUpIndex)) {
final stabilizedMap = _stabilizeOptimisticSelfMicSeat(
nextMap: nextMap,
previousMap: previousMap,
selfSeatIndices: selfSeatIndices,
targetIndex: goUpIndex,
currentUserId: currentUserId,
);
if (stabilizedMap != null) {
return stabilizedMap;
}
} else {
_clearSelfMicGoUpGuard();
}
if (!hasActiveGuard) { if (!hasActiveGuard) {
_clearSelfMicSwitchGuard(); _clearSelfMicSwitchGuard();
} else { } else {
@ -1320,50 +1366,15 @@ class RealTimeCommunicationManager extends ChangeNotifier {
(seatIndex) => seatIndex == resolvedSourceIndex, (seatIndex) => seatIndex == resolvedSourceIndex,
); );
if (selfOnlyOnSource) { if (selfOnlyOnSource) {
final optimisticTargetSeat = previousMap[resolvedTargetIndex]; final stabilizedMap = _stabilizeOptimisticSelfMicSeat(
if (optimisticTargetSeat != null && nextMap: nextMap,
optimisticTargetSeat.user?.id == currentUserId) { previousMap: previousMap,
final incomingTargetSeat = nextMap[resolvedTargetIndex]; selfSeatIndices: selfSeatIndices,
if (incomingTargetSeat?.user == null || targetIndex: resolvedTargetIndex,
incomingTargetSeat?.user?.id == currentUserId) { currentUserId: currentUserId,
final stabilizedMap = Map<num, MicRes>.from(nextMap); );
for (final seatIndex in selfSeatIndices) { if (stabilizedMap != null) {
if (seatIndex == resolvedTargetIndex) { return stabilizedMap;
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;
}
} }
} }
} }
@ -1397,6 +1408,60 @@ class RealTimeCommunicationManager extends ChangeNotifier {
return stabilizedMap; return stabilizedMap;
} }
Map<num, MicRes>? _stabilizeOptimisticSelfMicSeat({
required Map<num, MicRes> nextMap,
required Map<num, MicRes> previousMap,
required Iterable<num> selfSeatIndices,
required num targetIndex,
required String currentUserId,
}) {
final optimisticTargetSeat = previousMap[targetIndex];
if (optimisticTargetSeat == null ||
optimisticTargetSeat.user?.id != currentUserId) {
return null;
}
final incomingTargetSeat = nextMap[targetIndex];
if (incomingTargetSeat?.user != null &&
incomingTargetSeat?.user?.id != currentUserId) {
return null;
}
final stabilizedMap = Map<num, MicRes>.from(nextMap);
for (final seatIndex in selfSeatIndices) {
if (seatIndex == targetIndex) {
continue;
}
final seat = stabilizedMap[seatIndex];
if (seat == null) {
continue;
}
stabilizedMap[seatIndex] = seat.copyWith(clearUser: true);
}
final baseSeat = incomingTargetSeat ?? optimisticTargetSeat;
stabilizedMap[targetIndex] = baseSeat.copyWith(
user: optimisticTargetSeat.user,
micMute: incomingTargetSeat?.micMute ?? optimisticTargetSeat.micMute,
micLock: incomingTargetSeat?.micLock ?? optimisticTargetSeat.micLock,
roomToken:
incomingTargetSeat?.roomToken ?? optimisticTargetSeat.roomToken,
emojiPath:
(incomingTargetSeat?.emojiPath ?? "").isNotEmpty
? incomingTargetSeat?.emojiPath
: optimisticTargetSeat.emojiPath,
type:
(incomingTargetSeat?.type ?? "").isNotEmpty
? incomingTargetSeat?.type
: optimisticTargetSeat.type,
number:
(incomingTargetSeat?.number ?? "").isNotEmpty
? incomingTargetSeat?.number
: optimisticTargetSeat.number,
);
return stabilizedMap;
}
bool _shouldPreferDuplicateMicSeat({ bool _shouldPreferDuplicateMicSeat({
required String userId, required String userId,
required num existingIndex, required num existingIndex,
@ -1477,6 +1542,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
if (currentUserMic == null) { if (currentUserMic == null) {
_clearSelfMicSwitchGuard(clearPreferredIndex: true); _clearSelfMicSwitchGuard(clearPreferredIndex: true);
_clearSelfMicGoUpGuard(clearPreferredIndex: true);
_roomRtcRoleIsBroadcaster = false; _roomRtcRoleIsBroadcaster = false;
_syncRoomHeartbeatState( _syncRoomHeartbeatState(
roomId: roomId, roomId: roomId,
@ -2443,6 +2509,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
final currentSeatIndex = userOnMaiInIndex(currentUserId); final currentSeatIndex = userOnMaiInIndex(currentUserId);
isMic = true; isMic = true;
_clearSelfMicSwitchGuard(clearPreferredIndex: true); _clearSelfMicSwitchGuard(clearPreferredIndex: true);
_clearSelfMicGoUpGuard(clearPreferredIndex: true);
if (currentSeatIndex > -1) { if (currentSeatIndex > -1) {
_startSelfMicReleaseGuard(sourceIndex: currentSeatIndex); _startSelfMicReleaseGuard(sourceIndex: currentSeatIndex);
} else { } else {
@ -4966,6 +5033,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
needUpDataUserInfo = false; needUpDataUserInfo = false;
SCRoomUtils.roomUsersMap.clear(); SCRoomUtils.roomUsersMap.clear();
_clearSelfMicSwitchGuard(clearPreferredIndex: true); _clearSelfMicSwitchGuard(clearPreferredIndex: true);
_clearSelfMicGoUpGuard(clearPreferredIndex: true);
_clearSelfMicReleaseGuard(); _clearSelfMicReleaseGuard();
_seatResizeOccupantPreserveUntilMs = null; _seatResizeOccupantPreserveUntilMs = null;
roomIsMute = false; roomIsMute = false;
@ -5215,6 +5283,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
); );
} catch (downError) {} } catch (downError) {}
_clearSelfMicSwitchGuard(clearPreferredIndex: true); _clearSelfMicSwitchGuard(clearPreferredIndex: true);
_clearSelfMicGoUpGuard(clearPreferredIndex: true);
_clearUserFromSeats(myUser?.id); _clearUserFromSeats(myUser?.id);
_syncSelfMicRuntimeState(); _syncSelfMicRuntimeState();
notifyListeners(); notifyListeners();
@ -5237,8 +5306,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
targetIndex: targetIndex, targetIndex: targetIndex,
); );
} else { } else {
_preferredSelfMicIndex = targetIndex;
_clearSelfMicSwitchGuard(); _clearSelfMicSwitchGuard();
_startSelfMicGoUpGuard(targetIndex: targetIndex);
} }
_clearUserFromSeats(myUser.id, exceptIndex: targetIndex); _clearUserFromSeats(myUser.id, exceptIndex: targetIndex);
final currentSeat = roomWheatMap[targetIndex]; final currentSeat = roomWheatMap[targetIndex];
@ -5302,6 +5371,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
isMic = true; isMic = true;
} }
_clearSelfMicSwitchGuard(clearPreferredIndex: true); _clearSelfMicSwitchGuard(clearPreferredIndex: true);
_clearSelfMicGoUpGuard(clearPreferredIndex: true);
if (currentUserSeatIndex > -1) { if (currentUserSeatIndex > -1) {
_startSelfMicReleaseGuard(sourceIndex: currentUserSeatIndex); _startSelfMicReleaseGuard(sourceIndex: currentUserSeatIndex);
} else { } else {

View File

@ -0,0 +1,33 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:yumi/services/audio/rtc_manager.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/data_sources/sources/local/user_manager.dart';
void main() {
test('self go-up keeps optimistic mic seat over stale mic list data', () {
SharedPreferences.setMockInitialValues({});
AccountStorage().setCurrentUser(
SocialChatLoginRes(
userProfile: SocialChatUserProfile(id: 'user-1', userNickname: 'Me'),
),
);
final rtcProvider =
RealTimeCommunicationManager()
..roomWheatMap = {
3: MicRes(
micIndex: 3,
user: SocialChatUserProfile(id: 'user-1', userNickname: 'Me'),
),
};
rtcProvider.debugStartSelfMicGoUpGuard(3);
rtcProvider.debugApplyMicListSnapshot([
MicRes(micIndex: 3),
], notifyIfUnchanged: false);
expect(rtcProvider.roomWheatMap[3]?.user?.id, 'user-1');
expect(rtcProvider.isOnMai(), isTrue);
});
}