diff --git a/lib/modules/room/voice_room_page.dart b/lib/modules/room/voice_room_page.dart index b6145cf..71d62a6 100644 --- a/lib/modules/room/voice_room_page.dart +++ b/lib/modules/room/voice_room_page.dart @@ -560,7 +560,6 @@ class _VoiceRoomPageState extends State ///礼物上飘动画 _floatingGiftListener(Msg msg) { final rtcProvider = Provider.of(context, listen: false); - rtcProvider.scheduleRoomRocketStatusRefreshForGift(); if (!rtcProvider.shouldShowRoomVisualEffects) { return; } diff --git a/lib/services/audio/rtc_manager.dart b/lib/services/audio/rtc_manager.dart index 6e7fec3..1e879af 100644 --- a/lib/services/audio/rtc_manager.dart +++ b/lib/services/audio/rtc_manager.dart @@ -304,6 +304,12 @@ class RealTimeCommunicationManager extends ChangeNotifier { Duration(seconds: 5), Duration(seconds: 10), ]; + static const List _roomRocketEntryReadyFallbackDelays = [ + Duration(milliseconds: 300), + Duration(milliseconds: 900), + Duration(milliseconds: 1800), + Duration(milliseconds: 3200), + ]; static const int _roomPasswordNotTrueErrorCode = 9005; static const Duration _micListPollingInterval = Duration(seconds: 2); @@ -345,6 +351,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { Timer? _roomRocketLaunchAnimationTimer; Timer? _roomRocketGiftRefreshTimer; final List _roomRocketPostLaunchRefreshTimers = []; + final List _roomRocketEntryReadyFallbackTimers = []; Timer? _roomRedPacketPresenceTimer; Future? _rtcEnginePrewarmTask; Future? _pendingRoomSwitchRtcLeaveTask; @@ -492,6 +499,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { _roomRocketLaunchAnimationTimer = null; _roomRocketGiftRefreshTimer?.cancel(); _roomRocketGiftRefreshTimer = null; + _cancelRoomRocketEntryReadyFallbacks(); _cancelRoomRocketPostLaunchStatusRefresh(); } _roomVisualEffectsEnabled = enabled; @@ -503,7 +511,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { if (visible) { final roomId = (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(); if (roomId.isNotEmpty) { - rtmProvider?.handleVoiceRoomReadyForRocketRewards(roomId); + _scheduleRoomRocketEntryReadyFallback(roomId); } } } @@ -2600,6 +2608,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { setRoomVisualEffectsEnabled(true); if (shouldOpenRoomPage) { VoiceRoomRoute.openVoiceRoom(context!); + setVoiceRoomRouteVisible(true); } unawaited( _bootstrapEnteredVoiceRoomSession( @@ -2700,7 +2709,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { roomId: roomId, ); _loadRoomSecondaryData(); - rtmProvider?.handleVoiceRoomReadyForRocketRewards(roomId); + _scheduleRoomRocketEntryReadyFallback(roomId); } catch (error, stackTrace) { if (!_isActiveRoomStartup(entryRequestSerial, roomId)) { return; @@ -2822,6 +2831,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { roomRocketStatus = res; notifyListeners(); _scheduleRoomRocketAssetPreload(roomId, res, afterRoomEntry: true); + _scheduleRoomRocketEntryReadyFallback(roomId); }) .catchError((e) {}); _loadRoomRocketRewardPopups(roomId); @@ -2944,6 +2954,42 @@ class RealTimeCommunicationManager extends ChangeNotifier { }); } + void _scheduleRoomRocketEntryReadyFallback(String roomId) { + final normalizedRoomId = roomId.trim(); + if (normalizedRoomId.isEmpty) { + return; + } + _notifyRoomRocketEntryReadyIfCurrent(normalizedRoomId); + for (final delay in _roomRocketEntryReadyFallbackDelays) { + final timer = Timer(delay, () { + _roomRocketEntryReadyFallbackTimers.removeWhere( + (item) => !item.isActive, + ); + _notifyRoomRocketEntryReadyIfCurrent(normalizedRoomId); + }); + _roomRocketEntryReadyFallbackTimers.add(timer); + } + } + + void _notifyRoomRocketEntryReadyIfCurrent(String roomId) { + final normalizedRoomId = roomId.trim(); + final currentRoomId = + currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ""; + if (normalizedRoomId.isEmpty || + currentRoomId != normalizedRoomId || + !_voiceRoomRouteVisible) { + return; + } + rtmProvider?.handleVoiceRoomReadyForRocketRewards(normalizedRoomId); + } + + void _cancelRoomRocketEntryReadyFallbacks() { + for (final timer in _roomRocketEntryReadyFallbackTimers) { + timer.cancel(); + } + _roomRocketEntryReadyFallbackTimers.clear(); + } + void scheduleRoomRocketPostLaunchStatusRefresh({String? roomId}) { final resolvedRoomId = (roomId ?? currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(); @@ -3033,7 +3079,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { builder: (_) => RoomRocketPagEffectOverlay( resource: animationUrl, - topAvatarUrl: _roomRocketTop1AvatarUrl(), + topAvatarUrl: _roomRocketTop1AvatarUrl(launch.safeLevel), onCompleted: () { SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag); _roomRocketLaunchAnimationTimer?.cancel(); @@ -3054,8 +3100,12 @@ class RealTimeCommunicationManager extends ChangeNotifier { ); } - String? _roomRocketTop1AvatarUrl() { - final kings = roomRocketStatus?.rocketKings ?? const []; + String? _roomRocketTop1AvatarUrl(int level) { + final levelKings = roomRocketStatus?.rocketKingsForLevel(level) ?? const []; + final kings = + levelKings.isNotEmpty + ? levelKings + : roomRocketStatus?.rocketKings ?? const []; if (kings.isEmpty) { return null; } @@ -3374,9 +3424,6 @@ class RealTimeCommunicationManager extends ChangeNotifier { final exitRtmProvider = rtmProvider; final navigationContext = context; try { - exitRtmProvider?.msgAllListener = null; - exitRtmProvider?.msgChatListener = null; - exitRtmProvider?.msgGiftListener = null; SCGiftVapSvgaManager().stopPlayback(); setRoomVisualEffectsEnabled(false); SCHeartbeatUtils.cancelTimer(); @@ -3412,9 +3459,6 @@ class RealTimeCommunicationManager extends ChangeNotifier { final isPreviewOnlyExit = _currentRoomIsEntryPreview; final exitRtmProvider = rtmProvider; try { - exitRtmProvider?.msgAllListener = null; - exitRtmProvider?.msgChatListener = null; - exitRtmProvider?.msgGiftListener = null; SCGiftVapSvgaManager().stopPlayback(); setRoomVisualEffectsEnabled(false); SCHeartbeatUtils.cancelTimer(); diff --git a/lib/services/audio/rtm_manager.dart b/lib/services/audio/rtm_manager.dart index 0a47650..ec73314 100644 --- a/lib/services/audio/rtm_manager.dart +++ b/lib/services/audio/rtm_manager.dart @@ -135,6 +135,7 @@ class RealTimeMessagingManager extends ChangeNotifier { static const int _roomRocketRegionBroadcastRecentTtlMs = 90000; static const int _roomRocketRewardPendingTtlMs = 120000; static const int _roomRocketRewardProbeTtlMs = 120000; + static const int _roomRocketLaunchCountdownRetainMs = 10000; static const Duration _createRoomGroupTimeout = Duration(seconds: 12); static const List _roomRocketRewardPopupRetryDelays = [ Duration(milliseconds: 700), @@ -262,6 +263,12 @@ class RealTimeMessagingManager extends ChangeNotifier { final _pendingRoomRocketRewardPopups = {}; final Map _recentRoomRocketRewardProbeTimes = {}; + final Map _roomRocketLaunchCountdownDueTimes = {}; + final Map _roomRocketLaunchCountdownKeysByRoom = + {}; + final _pendingRoomRocketCountdownLaunches = + {}; + final Map _roomRocketLaunchEffectTimers = {}; Timer? _roomRocketRewardPopupRetryTimer; Timer? _roomRocketRoomReadyRewardRetryTimer; int get currentLuckGiftBurstPlaybackToken => _luckGiftPushPlaybackToken; @@ -1163,6 +1170,7 @@ class RealTimeMessagingManager extends ChangeNotifier { } addMsg(Msg msg) { + _scheduleRoomRocketStatusRefreshForGiftMessage(msg); final mergedGiftMsg = _mergeGiftMessageIfNeeded(msg); if (mergedGiftMsg != null) { msgAllListener?.call(mergedGiftMsg); @@ -1249,6 +1257,33 @@ class RealTimeMessagingManager extends ChangeNotifier { } } + void _scheduleRoomRocketStatusRefreshForGiftMessage(Msg msg) { + if (msg.type != SCRoomMsgType.gift && + msg.type != SCRoomMsgType.luckGiftAnimOther) { + return; + } + final currentContext = context; + if (currentContext == null || !currentContext.mounted) { + return; + } + final rtcProvider = Provider.of( + currentContext, + listen: false, + ); + final currentRoomId = + (rtcProvider.currenRoom?.roomProfile?.roomProfile?.id ?? '').trim(); + if (currentRoomId.isEmpty) { + return; + } + final messageRoomId = msg.type == SCRoomMsgType.gift ? msg.msg?.trim() : ''; + if (messageRoomId != null && + messageRoomId.isNotEmpty && + messageRoomId != currentRoomId) { + return; + } + rtcProvider.scheduleRoomRocketStatusRefreshForGift(roomId: currentRoomId); + } + void _cacheRoomRedPacketMessage(Msg msg) { try { final decoded = jsonDecode(msg.msg ?? ''); @@ -1985,6 +2020,7 @@ class RealTimeMessagingManager extends ChangeNotifier { publishRegionBroadcast: true, ); } else { + _rememberRoomRocketLaunchCountdown(launch); _markRoomRocketRewardProbe(launch.roomId); _publishRoomRocketRegionBroadcastIfNeeded(launch); } @@ -2002,6 +2038,7 @@ class RealTimeMessagingManager extends ChangeNotifier { if (!launch.isValid) { return; } + _rememberRoomRocketLaunchCountdown(launch); var isCurrentRoom = false; var isCurrentVisibleRoom = false; @@ -2092,14 +2129,22 @@ class RealTimeMessagingManager extends ChangeNotifier { final pending = _pendingRoomRocketRewardPopups[normalizedRoomId]; final hasProbe = _recentRoomRocketRewardProbeTimes[normalizedRoomId] != null; - if (pending == null && !hasProbe) { + final hasLaunch = + _pendingRoomRocketCountdownLaunches[normalizedRoomId] != null; + if (pending == null && !hasProbe && !hasLaunch) { return; } if (!_isCurrentVisibleVoiceRoom(normalizedRoomId)) { _scheduleRoomRocketReadyRewardRetry(normalizedRoomId, attempt); return; } - if (pending == null && attempt == 0) { + final resumedLaunch = _resumeRoomRocketLaunchForCurrentRoom( + normalizedRoomId, + ); + if (pending == null && !hasProbe) { + return; + } + if (!resumedLaunch && pending == null && attempt == 0) { _scheduleRoomRocketReadyRewardRetry(normalizedRoomId, attempt); return; } @@ -2180,15 +2225,218 @@ class RealTimeMessagingManager extends ChangeNotifier { _recentRoomRocketRewardProbeTimes.clear(); _roomRocketRoomReadyRewardRetryTimer?.cancel(); _roomRocketRoomReadyRewardRetryTimer = null; + _clearRoomRocketLaunchCountdownState(); + } + + void _rememberRoomRocketLaunchCountdown( + RoomRocketLaunchBroadcastMessage launch, + ) { + if (!launch.isValid) { + return; + } + final now = DateTime.now().millisecondsSinceEpoch; + _pruneRoomRocketLaunchCountdowns(now); + final key = _roomRocketLaunchCountdownKey(launch); + final existingDueAt = _roomRocketLaunchCountdownDueTimes[key]; + final dueAt = + existingDueAt != null && existingDueAt > now + ? existingDueAt + : now + _roomRocketLaunchCountdownDelay(launch).inMilliseconds; + _roomRocketLaunchCountdownDueTimes[key] = dueAt; + _roomRocketLaunchCountdownKeysByRoom[launch.roomId] = key; + _pendingRoomRocketCountdownLaunches[launch.roomId] = launch; + } + + void _scheduleRoomRocketLaunchEffectsAfterCountdown( + RoomRocketLaunchBroadcastMessage launch, + ) { + if (!launch.isValid) { + return; + } + _rememberRoomRocketLaunchCountdown(launch); + final key = _roomRocketLaunchCountdownKey(launch); + final delay = _roomRocketLaunchCountdownRemaining(launch.roomId); + final dueAt = + _roomRocketLaunchCountdownDueTimes[key] ?? + DateTime.now().millisecondsSinceEpoch; + _roomRocketLaunchEffectTimers[key]?.cancel(); + _roomRocketLaunchEffectTimers[key] = Timer(delay, () { + _roomRocketLaunchEffectTimers.remove(key); + if (_roomRocketLaunchCountdownDueTimes[key] != dueAt) { + return; + } + _roomRocketLaunchCountdownDueTimes.remove(key); + if (_roomRocketLaunchCountdownKeysByRoom[launch.roomId] == key) { + _roomRocketLaunchCountdownKeysByRoom.remove(launch.roomId); + } + final pendingLaunch = + _pendingRoomRocketCountdownLaunches.remove(launch.roomId) ?? launch; + _playRoomRocketLaunchEffectsAndReward(pendingLaunch); + }); + } + + void _playRoomRocketLaunchEffectsAndReward( + RoomRocketLaunchBroadcastMessage launch, + ) { + _clearRoomRocketLaunchCountdownForRoom(launch.roomId); + if (!_isCurrentVisibleVoiceRoom(launch.roomId)) { + _markRoomRocketRewardProbe(launch.roomId); + return; + } + final currentContext = context; + if (currentContext == null || !currentContext.mounted) { + _markRoomRocketRewardProbe(launch.roomId); + return; + } + final rtcProvider = Provider.of( + currentContext, + listen: false, + ); + final shouldFetchReward = + _pendingRoomRocketRewardPopups.containsKey(launch.roomId) || + _recentRoomRocketRewardProbeTimes.containsKey(launch.roomId); + final pending = _pendingRoomRocketRewardPopups.remove(launch.roomId); + _recentRoomRocketRewardProbeTimes.remove(launch.roomId); + rtcProvider.handleRoomRocketLaunchBroadcast(launch); + if (shouldFetchReward) { + unawaited( + _showRoomRocketRewardPopupDialog( + launch.roomId, + initialRoomRecords: + pending?.initialRoomRecords ?? + const [], + ), + ); + } + } + + bool _resumeRoomRocketLaunchForCurrentRoom(String roomId) { + final normalizedRoomId = roomId.trim(); + final launch = _pendingRoomRocketCountdownLaunches[normalizedRoomId]; + if (launch == null || !_isCurrentVisibleVoiceRoom(normalizedRoomId)) { + return false; + } + final currentContext = context; + if (currentContext == null || !currentContext.mounted) { + return false; + } + final rtcProvider = Provider.of( + currentContext, + listen: false, + ); + final remaining = _roomRocketLaunchCountdownRemaining(normalizedRoomId); + if (remaining > Duration.zero) { + _addRoomRocketLaunchNoticeIfNeeded( + rtcProvider: rtcProvider, + launch: launch, + secondsOverride: _roomRocketRemainingSeconds(remaining), + ); + _scheduleRoomRocketLaunchEffectsAfterCountdown(launch); + } else { + _playRoomRocketLaunchEffectsAndReward(launch); + } + return true; + } + + int _roomRocketRemainingSeconds(Duration duration) { + final seconds = (duration.inMilliseconds / 1000).ceil(); + return seconds.clamp(1, 60).toInt(); + } + + Duration _roomRocketLaunchCountdownRemaining(String roomId) { + final normalizedRoomId = roomId.trim(); + if (normalizedRoomId.isEmpty) { + return Duration.zero; + } + final now = DateTime.now().millisecondsSinceEpoch; + _pruneRoomRocketLaunchCountdowns(now); + final key = _roomRocketLaunchCountdownKeysByRoom[normalizedRoomId]; + final dueAt = key == null ? null : _roomRocketLaunchCountdownDueTimes[key]; + if (dueAt == null || dueAt <= now) { + return Duration.zero; + } + return Duration(milliseconds: dueAt - now); + } + + Duration _roomRocketLaunchCountdownDelay( + RoomRocketLaunchBroadcastMessage launch, + ) { + final seconds = launch.durationSeconds <= 0 ? 10 : launch.durationSeconds; + return Duration(seconds: seconds.clamp(0, 60).toInt()); + } + + String _roomRocketLaunchCountdownKey( + RoomRocketLaunchBroadcastMessage launch, + ) { + final launchKey = _roomRocketLaunchNoticeKey(launch); + if (launchKey.isNotEmpty) { + return launchKey; + } + return 'rocket_launch_countdown|${launch.roomId}'; + } + + void _pruneRoomRocketLaunchCountdowns(int now) { + final expiredKeys = {}; + _roomRocketLaunchCountdownDueTimes.removeWhere((key, dueAt) { + final expired = now - dueAt > _roomRocketLaunchCountdownRetainMs; + if (expired) { + expiredKeys.add(key); + } + return expired; + }); + if (expiredKeys.isEmpty) { + return; + } + _roomRocketLaunchCountdownKeysByRoom.removeWhere( + (_, key) => expiredKeys.contains(key), + ); + _pendingRoomRocketCountdownLaunches.removeWhere( + (roomId, _) => !_roomRocketLaunchCountdownKeysByRoom.containsKey(roomId), + ); + } + + void _clearRoomRocketLaunchCountdownState() { + _roomRocketLaunchCountdownDueTimes.clear(); + _roomRocketLaunchCountdownKeysByRoom.clear(); + _pendingRoomRocketCountdownLaunches.clear(); + for (final timer in _roomRocketLaunchEffectTimers.values) { + timer.cancel(); + } + _roomRocketLaunchEffectTimers.clear(); + } + + void _clearRoomRocketLaunchCountdownForRoom(String roomId) { + final normalizedRoomId = roomId.trim(); + final key = _roomRocketLaunchCountdownKeysByRoom.remove(normalizedRoomId); + if (key != null) { + _roomRocketLaunchCountdownDueTimes.remove(key); + _roomRocketLaunchEffectTimers.remove(key)?.cancel(); + } + _pendingRoomRocketCountdownLaunches.remove(normalizedRoomId); } void _handleCurrentVisibleRoomRocketLaunch({ required RealTimeCommunicationManager rtcProvider, required RoomRocketLaunchBroadcastMessage launch, bool publishRegionBroadcast = false, + }) { + if (_addRoomRocketLaunchNoticeIfNeeded( + rtcProvider: rtcProvider, + launch: launch, + publishRegionBroadcast: publishRegionBroadcast, + )) { + _scheduleRoomRocketLaunchEffectsAfterCountdown(launch); + } + } + + bool _addRoomRocketLaunchNoticeIfNeeded({ + required RealTimeCommunicationManager rtcProvider, + required RoomRocketLaunchBroadcastMessage launch, + bool publishRegionBroadcast = false, + int? secondsOverride, }) { if (!_isCurrentVisibleVoiceRoom(launch.roomId)) { - return; + return false; } final launchKey = _roomRocketLaunchNoticeKey(launch); final looseLaunchKey = _roomRocketLaunchLooseKey(launch); @@ -2199,26 +2447,27 @@ class RealTimeMessagingManager extends ChangeNotifier { _recentRoomRocketLaunchTimes.containsKey(launchKey)) || (looseLaunchKey.isNotEmpty && _recentRoomRocketLaunchLooseTimes.containsKey(looseLaunchKey)); - if (!alreadyHandled && launchKey.isNotEmpty) { - _recentRoomRocketLaunchTimes[launchKey] = now; - if (looseLaunchKey.isNotEmpty) { - _recentRoomRocketLaunchLooseTimes[looseLaunchKey] = now; - } - addMsg( - _buildRoomRocketLaunchNoticeMsg( - _firstNonBlank([ - rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount, - launch.roomAccount, - ]), - launch, - ), - ); - if (publishRegionBroadcast) { - _publishRoomRocketRegionBroadcastIfNeeded(launch); - } - rtcProvider.handleRoomRocketLaunchBroadcast(launch); + if (alreadyHandled || launchKey.isEmpty) { + return false; } - unawaited(_showRoomRocketRewardPopupDialog(launch.roomId)); + _recentRoomRocketLaunchTimes[launchKey] = now; + if (looseLaunchKey.isNotEmpty) { + _recentRoomRocketLaunchLooseTimes[looseLaunchKey] = now; + } + addMsg( + _buildRoomRocketLaunchNoticeMsg( + _firstNonBlank([ + rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount, + launch.roomAccount, + ]), + launch, + secondsOverride: secondsOverride, + ), + ); + if (publishRegionBroadcast) { + _publishRoomRocketRegionBroadcastIfNeeded(launch); + } + return true; } void _publishRoomRocketRegionBroadcastIfNeeded( @@ -2453,9 +2702,12 @@ class RealTimeMessagingManager extends ChangeNotifier { Msg _buildRoomRocketLaunchNoticeMsg( String groupId, - RoomRocketLaunchBroadcastMessage launch, - ) { - final seconds = launch.durationSeconds <= 0 ? 10 : launch.durationSeconds; + RoomRocketLaunchBroadcastMessage launch, { + int? secondsOverride, + }) { + final seconds = + secondsOverride ?? + (launch.durationSeconds <= 0 ? 10 : launch.durationSeconds); return Msg( groupId: groupId, msg: seconds.toString(),