diff --git a/lib/services/audio/rtc_manager.dart b/lib/services/audio/rtc_manager.dart index 55e052c..bd7939d 100644 --- a/lib/services/audio/rtc_manager.dart +++ b/lib/services/audio/rtc_manager.dart @@ -428,6 +428,8 @@ class RealTimeCommunicationManager extends ChangeNotifier { final Map _recentMicChangeSeatSnapshots = {}; num? _preferredSelfMicIndex; + num? _pendingSelfMicGoUpIndex; + int? _pendingSelfMicGoUpGuardUntilMs; num? _pendingSelfMicSourceIndex; int? _pendingSelfMicSwitchGuardUntilMs; num? _pendingSelfMicReleaseIndex; @@ -1218,6 +1220,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { required num sourceIndex, required num targetIndex, }) { + _clearSelfMicGoUpGuard(); _clearSelfMicReleaseGuard(); _preferredSelfMicIndex = targetIndex; _pendingSelfMicSourceIndex = sourceIndex; @@ -1225,6 +1228,22 @@ class RealTimeCommunicationManager extends ChangeNotifier { 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}) { _pendingSelfMicSourceIndex = null; _pendingSelfMicSwitchGuardUntilMs = null; @@ -1233,6 +1252,11 @@ class RealTimeCommunicationManager extends ChangeNotifier { } } + @visibleForTesting + void debugStartSelfMicGoUpGuard(num targetIndex) { + _startSelfMicGoUpGuard(targetIndex: targetIndex); + } + void _startSelfMicReleaseGuard({required num sourceIndex}) { _pendingSelfMicReleaseIndex = sourceIndex; _pendingSelfMicReleaseGuardUntilMs = @@ -1305,9 +1329,31 @@ class RealTimeCommunicationManager extends ChangeNotifier { if (targetIndex != null && nextMap[targetIndex]?.user?.id == currentUserId) { _clearSelfMicSwitchGuard(); + _clearSelfMicGoUpGuard(); 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) { _clearSelfMicSwitchGuard(); } else { @@ -1320,50 +1366,15 @@ class RealTimeCommunicationManager extends ChangeNotifier { (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.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 stabilizedMap = _stabilizeOptimisticSelfMicSeat( + nextMap: nextMap, + previousMap: previousMap, + selfSeatIndices: selfSeatIndices, + targetIndex: resolvedTargetIndex, + currentUserId: currentUserId, + ); + if (stabilizedMap != null) { + return stabilizedMap; } } } @@ -1397,6 +1408,60 @@ class RealTimeCommunicationManager extends ChangeNotifier { return stabilizedMap; } + Map? _stabilizeOptimisticSelfMicSeat({ + required Map nextMap, + required Map previousMap, + required Iterable 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.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({ required String userId, required num existingIndex, @@ -1477,6 +1542,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { if (currentUserMic == null) { _clearSelfMicSwitchGuard(clearPreferredIndex: true); + _clearSelfMicGoUpGuard(clearPreferredIndex: true); _roomRtcRoleIsBroadcaster = false; _syncRoomHeartbeatState( roomId: roomId, @@ -2443,6 +2509,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { final currentSeatIndex = userOnMaiInIndex(currentUserId); isMic = true; _clearSelfMicSwitchGuard(clearPreferredIndex: true); + _clearSelfMicGoUpGuard(clearPreferredIndex: true); if (currentSeatIndex > -1) { _startSelfMicReleaseGuard(sourceIndex: currentSeatIndex); } else { @@ -4966,6 +5033,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { needUpDataUserInfo = false; SCRoomUtils.roomUsersMap.clear(); _clearSelfMicSwitchGuard(clearPreferredIndex: true); + _clearSelfMicGoUpGuard(clearPreferredIndex: true); _clearSelfMicReleaseGuard(); _seatResizeOccupantPreserveUntilMs = null; roomIsMute = false; @@ -5215,6 +5283,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { ); } catch (downError) {} _clearSelfMicSwitchGuard(clearPreferredIndex: true); + _clearSelfMicGoUpGuard(clearPreferredIndex: true); _clearUserFromSeats(myUser?.id); _syncSelfMicRuntimeState(); notifyListeners(); @@ -5237,8 +5306,8 @@ class RealTimeCommunicationManager extends ChangeNotifier { targetIndex: targetIndex, ); } else { - _preferredSelfMicIndex = targetIndex; _clearSelfMicSwitchGuard(); + _startSelfMicGoUpGuard(targetIndex: targetIndex); } _clearUserFromSeats(myUser.id, exceptIndex: targetIndex); final currentSeat = roomWheatMap[targetIndex]; @@ -5302,6 +5371,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { isMic = true; } _clearSelfMicSwitchGuard(clearPreferredIndex: true); + _clearSelfMicGoUpGuard(clearPreferredIndex: true); if (currentUserSeatIndex > -1) { _startSelfMicReleaseGuard(sourceIndex: currentUserSeatIndex); } else { diff --git a/test/rtc_self_mic_snapshot_test.dart b/test/rtc_self_mic_snapshot_test.dart new file mode 100644 index 0000000..e767f29 --- /dev/null +++ b/test/rtc_self_mic_snapshot_test.dart @@ -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); + }); +}