From 15eb93c810bc01cabc4980179d34856b7b49beb3 Mon Sep 17 00:00:00 2001 From: zhx Date: Tue, 26 May 2026 16:36:07 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=87=AA=E5=B7=B1=E5=88=9A?= =?UTF-8?q?=E4=B8=8A=E9=BA=A6=E6=88=90=E5=8A=9F=E5=90=8E=EF=BC=8C=E4=BC=9A?= =?UTF-8?q?=E7=AB=8B=E5=8D=B3=E8=A7=A6=E5=8F=91=E4=B8=80=E6=AC=A1=20/live/?= =?UTF-8?q?mic/list=20=E5=88=B7=E6=96=B0=E3=80=82=E5=A6=82=E6=9E=9C?= =?UTF-8?q?=E8=BF=99=E6=AC=A1=E6=8B=89=E5=88=B0=E7=9A=84=E6=98=AF=E6=97=A7?= =?UTF-8?q?=E9=BA=A6=E4=BD=8D=E5=BF=AB=E7=85=A7=EF=BC=8C=E6=9C=AC=E5=9C=B0?= =?UTF-8?q?=20roomWheatMap=20=E4=BC=9A=E6=8A=8A=E2=80=9C=E8=87=AA=E5=B7=B1?= =?UTF-8?q?=E5=9C=A8=E9=BA=A6=E4=B8=8A=E2=80=9D=E7=9A=84=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E6=B8=85=E6=8E=89=EF=BC=8CisOnMai()=20=E5=8F=98=E6=88=90=20fal?= =?UTF-8?q?se=EF=BC=8C=E5=BA=95=E9=83=A8=E6=A0=8F=E9=BA=A6=E5=85=8B?= =?UTF-8?q?=E9=A3=8E=E5=B0=B1=E8=A2=AB=E9=9A=90=E8=97=8F=E4=BA=86=E3=80=82?= =?UTF-8?q?=E4=B9=8B=E5=89=8D=E5=8F=AA=E4=BF=9D=E6=8A=A4=E4=BA=86=20MIC=5F?= =?UTF-8?q?CHANGE=20=E5=92=8C=E6=8D=A2=E9=BA=A6=E4=BD=8D=E5=9C=BA=E6=99=AF?= =?UTF-8?q?=EF=BC=8C=E6=B2=A1=E6=9C=89=E4=BF=9D=E6=8A=A4=E2=80=9C=E8=87=AA?= =?UTF-8?q?=E5=B7=B1=E5=88=9D=E6=AC=A1=E4=B8=8A=E9=BA=A6=E2=80=9D=E7=9A=84?= =?UTF-8?q?=E6=97=A7=E5=BF=AB=E7=85=A7=E8=A6=86=E7=9B=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/services/audio/rtc_manager.dart | 160 +++++++++++++++++++-------- test/rtc_self_mic_snapshot_test.dart | 33 ++++++ 2 files changed, 148 insertions(+), 45 deletions(-) create mode 100644 test/rtc_self_mic_snapshot_test.dart 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); + }); +}