diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index bef7ba5..d2634e7 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -6,9 +6,13 @@
-
+
+
+
+
+
@@ -130,6 +134,12 @@
android:name="io.agora.rtc2.extensions.MediaProjectionMgr$LocalScreenSharingService"
tools:node="remove" />
+
+
diff --git a/android/app/src/main/kotlin/com/org/yumiparty/MainActivity.kt b/android/app/src/main/kotlin/com/org/yumiparty/MainActivity.kt
index 15a39dc..d935d00 100644
--- a/android/app/src/main/kotlin/com/org/yumiparty/MainActivity.kt
+++ b/android/app/src/main/kotlin/com/org/yumiparty/MainActivity.kt
@@ -30,6 +30,33 @@ class MainActivity : FlutterActivity() {
else -> result.notImplemented()
}
}
+ MethodChannel(
+ flutterEngine.dartExecutor.binaryMessenger,
+ VOICE_ROOM_FOREGROUND_CHANNEL
+ ).setMethodCallHandler { call, result ->
+ when (call.method) {
+ "start" -> {
+ try {
+ VoiceRoomForegroundService.start(
+ applicationContext,
+ call.argument("roomName")
+ )
+ result.success(null)
+ } catch (e: Exception) {
+ result.error("VOICE_ROOM_FOREGROUND_START_FAILED", e.message, null)
+ }
+ }
+ "stop" -> {
+ try {
+ VoiceRoomForegroundService.stop(applicationContext)
+ result.success(null)
+ } catch (e: Exception) {
+ result.error("VOICE_ROOM_FOREGROUND_STOP_FAILED", e.message, null)
+ }
+ }
+ else -> result.notImplemented()
+ }
+ }
}
override fun onDestroy() {
@@ -90,5 +117,7 @@ class MainActivity : FlutterActivity() {
companion object {
private const val INSTALL_REFERRER_CHANNEL = "com.org.yumiparty/install_referrer"
private const val MOBILE_CONTEXT_CHANNEL = "com.org.yumiparty/mobile_context"
+ private const val VOICE_ROOM_FOREGROUND_CHANNEL =
+ "com.org.yumiparty/voice_room_foreground_service"
}
}
diff --git a/android/app/src/main/kotlin/com/org/yumiparty/VoiceRoomForegroundService.kt b/android/app/src/main/kotlin/com/org/yumiparty/VoiceRoomForegroundService.kt
new file mode 100644
index 0000000..484ecad
--- /dev/null
+++ b/android/app/src/main/kotlin/com/org/yumiparty/VoiceRoomForegroundService.kt
@@ -0,0 +1,131 @@
+package com.org.yumiparty
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ServiceInfo
+import android.os.Build
+import android.os.IBinder
+
+class VoiceRoomForegroundService : Service() {
+ override fun onBind(intent: Intent?): IBinder? = null
+
+ override fun onCreate() {
+ super.onCreate()
+ ensureNotificationChannel()
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ val roomName = intent?.getStringExtra(EXTRA_ROOM_NAME)
+ ?.trim()
+ ?.takeIf { it.isNotEmpty() }
+ ?: DEFAULT_ROOM_NAME
+ startAsForeground(roomName)
+ return START_NOT_STICKY
+ }
+
+ private fun startAsForeground(roomName: String) {
+ val notification = buildNotification(roomName)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ val serviceType =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK or
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
+ } else {
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
+ }
+ startForeground(
+ NOTIFICATION_ID,
+ notification,
+ serviceType
+ )
+ } else {
+ startForeground(NOTIFICATION_ID, notification)
+ }
+ }
+
+ @Suppress("DEPRECATION")
+ private fun buildNotification(roomName: String): Notification {
+ val launchIntent = (
+ packageManager.getLaunchIntentForPackage(packageName)
+ ?: Intent(this, MainActivity::class.java)
+ ).apply {
+ addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
+ }
+ val pendingIntent = PendingIntent.getActivity(
+ this,
+ 0,
+ launchIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT or immutableFlag()
+ )
+
+ val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ Notification.Builder(this, CHANNEL_ID)
+ } else {
+ Notification.Builder(this)
+ }
+
+ return builder
+ .setSmallIcon(R.mipmap.ic_launcher)
+ .setContentTitle("Yumi voice room")
+ .setContentText(roomName)
+ .setOngoing(true)
+ .setShowWhen(false)
+ .setOnlyAlertOnce(true)
+ .setPriority(Notification.PRIORITY_LOW)
+ .setCategory(Notification.CATEGORY_CALL)
+ .setContentIntent(pendingIntent)
+ .build()
+ }
+
+ private fun ensureNotificationChannel() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
+ return
+ }
+ val manager = getSystemService(NotificationManager::class.java)
+ if (manager.getNotificationChannel(CHANNEL_ID) != null) {
+ return
+ }
+ val channel = NotificationChannel(
+ CHANNEL_ID,
+ "Voice room",
+ NotificationManager.IMPORTANCE_LOW
+ ).apply {
+ description = "Keeps voice rooms active while Yumi is in the background."
+ setShowBadge(false)
+ }
+ manager.createNotificationChannel(channel)
+ }
+
+ private fun immutableFlag(): Int =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ PendingIntent.FLAG_IMMUTABLE
+ } else {
+ 0
+ }
+
+ companion object {
+ private const val CHANNEL_ID = "yumi_voice_room"
+ private const val NOTIFICATION_ID = 24031
+ private const val EXTRA_ROOM_NAME = "room_name"
+ private const val DEFAULT_ROOM_NAME = "Voice room is running"
+
+ fun start(context: Context, roomName: String?) {
+ val intent = Intent(context, VoiceRoomForegroundService::class.java)
+ .putExtra(EXTRA_ROOM_NAME, roomName)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ context.startForegroundService(intent)
+ } else {
+ context.startService(intent)
+ }
+ }
+
+ fun stop(context: Context) {
+ context.stopService(Intent(context, VoiceRoomForegroundService::class.java))
+ }
+ }
+}
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index 365fcc7..fcd3e81 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -502,7 +502,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 10;
+ CURRENT_PROJECT_VERSION = 11;
DEVELOPMENT_TEAM = S9X2AJ2US9;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -693,7 +693,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 10;
+ CURRENT_PROJECT_VERSION = 11;
DEVELOPMENT_TEAM = F33K8VUZ62;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -722,7 +722,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 10;
+ CURRENT_PROJECT_VERSION = 11;
DEVELOPMENT_TEAM = F33K8VUZ62;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
diff --git a/lib/services/audio/rtc_manager.dart b/lib/services/audio/rtc_manager.dart
index 32b0b0e..09f85c2 100644
--- a/lib/services/audio/rtc_manager.dart
+++ b/lib/services/audio/rtc_manager.dart
@@ -15,6 +15,7 @@ import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository
import 'package:yumi/shared/data_sources/sources/repositories/sc_vip_repository_imp.dart';
import 'package:yumi/services/room/rc_room_manager.dart';
import 'package:yumi/services/audio/rtm_manager.dart';
+import 'package:yumi/services/audio/voice_room_foreground_service.dart';
import 'package:yumi/services/auth/user_profile_manager.dart';
import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart';
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
@@ -302,6 +303,11 @@ class RealTimeCommunicationManager extends ChangeNotifier {
'roomStartupSeatInteractionLoading';
static const String _selfMicGoUpRetryLoadingTag = 'selfMicGoUpRetryLoading';
static const int _selfMicGoUpFailureLimit = 3;
+ static const List _roomRedPacketEntryRefreshDelays = [
+ Duration.zero,
+ Duration(seconds: 2),
+ Duration(seconds: 6),
+ ];
bool needUpDataUserInfo = false;
bool _roomVisualEffectsEnabled = false;
@@ -443,6 +449,21 @@ class RealTimeCommunicationManager extends ChangeNotifier {
bool get shouldShowRoomVisualEffects =>
currenRoom != null && _roomVisualEffectsEnabled;
+ void _startVoiceRoomForegroundService() {
+ final roomProfile = currenRoom?.roomProfile?.roomProfile;
+ final roomName = (roomProfile?.roomName ?? "").trim();
+ final roomAccount = (roomProfile?.roomAccount ?? "").trim();
+ unawaited(
+ SCVoiceRoomForegroundService.start(
+ roomName: roomName.isNotEmpty ? roomName : roomAccount,
+ ),
+ );
+ }
+
+ void _stopVoiceRoomForegroundService() {
+ unawaited(SCVoiceRoomForegroundService.stop());
+ }
+
bool get _shouldDeferSeatInteractionForRoomStartup =>
_roomStartupStatus == RoomStartupStatus.loading;
@@ -1943,6 +1964,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
Future releaseRtcEngineForAppTermination() async {
final rtcEngine = engine;
if (rtcEngine == null) {
+ await SCVoiceRoomForegroundService.stop();
return;
}
@@ -1968,6 +1990,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
} catch (e) {
debugPrint('rtc释放引擎出错: $e');
}
+ await SCVoiceRoomForegroundService.stop();
}
void initializeAudioVolumeIndicationCallback(
@@ -2114,6 +2137,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
if (!context.mounted) {
return;
}
+ _startVoiceRoomForegroundService();
VoiceRoomRoute.openVoiceRoom(context);
return;
}
@@ -2137,6 +2161,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
_startRoomStatePolling();
_setRoomStartupReady();
setRoomVisualEffectsEnabled(true);
+ _startRoomRedPacketPresenceHeartbeat(roomId);
+ _refreshRoomRedPacketListAfterEntry(roomId: roomId);
+ _startVoiceRoomForegroundService();
VoiceRoomRoute.openVoiceRoom(context);
} else {
if (currenRoom != null) {
@@ -2157,6 +2184,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
previewData: previewData,
previewSeatCount: previewSeatCount,
);
+ _startVoiceRoomForegroundService();
VoiceRoomRoute.openVoiceRoom(context);
notifyListeners();
final entryRequestSerial = ++_roomEntryRequestSerial;
@@ -2192,6 +2220,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
}
currenRoom = _mergeVoiceRoomVipHints(enteredRoom, currenRoom);
_syncCurrentUserVipHintFromRoom(currenRoom);
+ _startVoiceRoomForegroundService();
unawaited(_refreshCurrentUserVipHintForRoom(entryRequestSerial));
_currentRoomIsEntryPreview = false;
_previewRoomSeatCount = null;
@@ -2207,6 +2236,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
return;
}
debugPrint('[RoomStartup] entry room failed: $e\n$stackTrace');
+ _stopVoiceRoomForegroundService();
_setRoomStartupFailed(RoomStartupFailureType.entry);
}
}
@@ -2674,6 +2704,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
debugPrint(
'[RoomStartup] join IM group failed code=${joinResult?.code} desc=${joinResult?.desc}',
);
+ _stopVoiceRoomForegroundService();
_setRoomStartupFailed(RoomStartupFailureType.im);
return;
}
@@ -2693,6 +2724,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
rtmProvider?.loadRoomHistoryMessages(groupId, count: 20) ??
Future.value(),
);
+ _refreshRoomRedPacketListAfterEntry(roomId: roomId);
await Future.wait([microphoneListFuture, onlineUsersFuture]);
if (!_isActiveRoomStartup(entryRequestSerial, roomId)) {
@@ -2748,6 +2780,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
return;
}
debugPrint('[RoomStartup] bootstrap failed: $error\n$stackTrace');
+ _stopVoiceRoomForegroundService();
_setRoomStartupFailed(failureType);
}
}
@@ -2988,11 +3021,17 @@ class RealTimeCommunicationManager extends ChangeNotifier {
Future> loadRoomRedPacketList(
int current,
) async {
+ final roomId = currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? "";
+ if (roomId.isEmpty) {
+ return const [];
+ }
var result = await SCChatRoomRepository().roomRedPacketList(
- currenRoom?.roomProfile?.roomProfile?.id ?? "",
+ roomId,
current,
);
- if (current == 1) {
+ final currentRoomId =
+ currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? "";
+ if (current == 1 && currentRoomId == roomId) {
redPacketList = result;
notifyListeners();
}
@@ -3055,19 +3094,18 @@ class RealTimeCommunicationManager extends ChangeNotifier {
);
}
- void _refreshRoomRedPacketListAfterEntry() {
- final roomId = currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? "";
- if (roomId.isEmpty) {
+ void _refreshRoomRedPacketListAfterEntry({String? roomId}) {
+ final normalizedRoomId =
+ roomId?.trim() ??
+ currenRoom?.roomProfile?.roomProfile?.id?.trim() ??
+ "";
+ if (normalizedRoomId.isEmpty) {
return;
}
- for (final delay in const [
- Duration.zero,
- Duration(seconds: 2),
- Duration(seconds: 6),
- ]) {
+ for (final delay in _roomRedPacketEntryRefreshDelays) {
unawaited(
Future.delayed(delay).then((_) {
- return _refreshRoomRedPacketListIfCurrent(roomId);
+ return _refreshRoomRedPacketListIfCurrent(normalizedRoomId);
}),
);
}
@@ -3381,6 +3419,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
///清空列表数据
void _clearData() {
_roomEntryRequestSerial += 1;
+ _stopVoiceRoomForegroundService();
_roomEntryEffectTimer?.cancel();
_roomEntryEffectTimer = null;
_stopRoomRedPacketPresenceHeartbeat();
diff --git a/lib/services/audio/rtm_manager.dart b/lib/services/audio/rtm_manager.dart
index 8cf06f7..ee17017 100644
--- a/lib/services/audio/rtm_manager.dart
+++ b/lib/services/audio/rtm_manager.dart
@@ -571,6 +571,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
logined = true;
print('tim 登录成功');
await initConversation();
+ unawaited(syncRoomRedPacketBroadcastGroup());
} else {
// 登录失败逻辑
//print('timm 需要重新登录2');
@@ -591,16 +592,26 @@ class RealTimeMessagingManager extends ChangeNotifier {
_onNewMessage(V2TimMessage message) {
if (message.groupID != null) {
+ final groupId = message.groupID!;
+
///全服通知 / 语区广播群
- if (message.groupID == SCGlobalConfig.bigBroadcastGroup ||
- message.groupID == roomRedPacketBroadcastGroupId) {
- _newBroadCastMsgRecv(message.groupID!, message);
+ if (groupId == SCGlobalConfig.bigBroadcastGroup ||
+ groupId == roomRedPacketBroadcastGroupId) {
+ _newBroadCastMsgRecv(groupId, message);
+ return;
+ }
+ if (_shouldRouteUntrackedRoomRedPacketBroadcast(groupId, message)) {
+ debugPrint(
+ '[RoomRedPacket][IM] route untracked group as region broadcast '
+ 'group=$groupId cachedRegionGroup=$roomRedPacketBroadcastGroupId',
+ );
+ _newBroadCastMsgRecv(groupId, message, forceRegionBroadcast: true);
return;
}
///群消息
for (var element in onNewMessageListenerGroupMap.values) {
- element?.call(message.groupID!, message);
+ element?.call(groupId, message);
}
} else {
///单聊消息
@@ -1780,6 +1791,38 @@ class RealTimeMessagingManager extends ChangeNotifier {
'';
}
+ String _currentVoiceRoomGroupId() {
+ final currentContext = context;
+ if (currentContext == null || !currentContext.mounted) {
+ return '';
+ }
+ return Provider.of(
+ currentContext,
+ listen: false,
+ ).currenRoom?.roomProfile?.roomProfile?.roomAccount?.trim() ??
+ '';
+ }
+
+ bool _shouldRouteUntrackedRoomRedPacketBroadcast(
+ String groupId,
+ V2TimMessage message,
+ ) {
+ if (groupId == _currentVoiceRoomGroupId()) {
+ return false;
+ }
+ final customData = message.customElem?.data;
+ if (customData == null || customData.isEmpty) {
+ return false;
+ }
+ try {
+ final decoded = json.decode(customData);
+ final payload = _broadcastPayloadMap(decoded);
+ return payload['type'] == SCRoomMsgType.roomRedPacket;
+ } catch (_) {
+ return false;
+ }
+ }
+
Map _broadcastPayloadMap(dynamic data) {
if (data is Map) {
return data;
@@ -1902,11 +1945,16 @@ class RealTimeMessagingManager extends ChangeNotifier {
}
///全服广播消息
- _newBroadCastMsgRecv(String groupID, V2TimMessage message) async {
+ _newBroadCastMsgRecv(
+ String groupID,
+ V2TimMessage message, {
+ bool forceRegionBroadcast = false,
+ }) async {
try {
String? customData = message.customElem?.data;
if (customData != null && customData.isNotEmpty) {
- final isRegionBroadcastGroup = groupID == roomRedPacketBroadcastGroupId;
+ final isRegionBroadcastGroup =
+ forceRegionBroadcast || groupID == roomRedPacketBroadcastGroupId;
final data = json.decode(customData);
var type = data["type"];
if (type == "SYS_ACTIVITY") {
@@ -1988,7 +2036,25 @@ class RealTimeMessagingManager extends ChangeNotifier {
} else if (type == SCRoomMsgType.roomRedPacket) {
///红包触发飘屏
var fData = data["data"];
+ final payload = _broadcastPayloadMap(fData);
+ final packetId = _payloadText(payload['packetId']);
+ final packetRoomId = _payloadText(payload["roomId"]);
+ debugPrint(
+ '[RoomRedPacket][IM] recv broadcast packetId=$packetId '
+ 'roomId=$packetRoomId group=$groupID '
+ 'regionGroup=$roomRedPacketBroadcastGroupId '
+ 'isRegion=$isRegionBroadcastGroup '
+ 'payloadRegion=${_payloadText(payload['regionCode'])} '
+ 'currentRegion=${roomRedPacketBroadcastRegionCode ?? ''} '
+ 'currentRoom=${_currentVoiceRoomId()}',
+ );
if (!_isSameRoomRedPacketRegion(fData)) {
+ debugPrint(
+ '[RoomRedPacket][IM] skip broadcast region mismatch '
+ 'packetId=$packetId '
+ 'payloadRegion=${_payloadText(payload['regionCode'])} '
+ 'currentRegion=${roomRedPacketBroadcastRegionCode ?? ''}',
+ );
return;
}
RoomRedPacketPendingCache.instance.upsertPayload(fData);
@@ -2007,7 +2073,6 @@ class RealTimeMessagingManager extends ChangeNotifier {
: '',
priority: 1000,
);
- final packetRoomId = _payloadText(fData["roomId"]);
if (_isCurrentVisibleVoiceRoom(packetRoomId)) {
final rtcProvider = Provider.of(
context!,
@@ -2616,6 +2681,12 @@ class RealTimeMessagingManager extends ChangeNotifier {
return;
}
+ debugPrint(
+ '[RoomRedPacket][IM] send broadcast packetId=${payload['packetId']} '
+ 'roomId=${payload['roomId']} mode=${payload['packetMode']} '
+ 'targets=${targetGroupIds.join(',')} regionGroup=$regionGroupId '
+ 'region=${payload['regionCode'] ?? roomRedPacketBroadcastRegionCode ?? ''}',
+ );
final message = {
'type': SCRoomMsgType.roomRedPacket,
'data': payload,
diff --git a/lib/services/audio/voice_room_foreground_service.dart b/lib/services/audio/voice_room_foreground_service.dart
new file mode 100644
index 0000000..9e164b5
--- /dev/null
+++ b/lib/services/audio/voice_room_foreground_service.dart
@@ -0,0 +1,38 @@
+import 'dart:io';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/services.dart';
+
+class SCVoiceRoomForegroundService {
+ static const MethodChannel _channel = MethodChannel(
+ 'com.org.yumiparty/voice_room_foreground_service',
+ );
+
+ static Future start({String? roomName}) async {
+ if (!Platform.isAndroid) {
+ return;
+ }
+ try {
+ await _channel.invokeMethod('start', {
+ 'roomName': roomName?.trim(),
+ });
+ } on PlatformException catch (e) {
+ debugPrint('[VoiceRoomForeground] start failed: ${e.message}');
+ } catch (e) {
+ debugPrint('[VoiceRoomForeground] start failed: $e');
+ }
+ }
+
+ static Future stop() async {
+ if (!Platform.isAndroid) {
+ return;
+ }
+ try {
+ await _channel.invokeMethod('stop');
+ } on PlatformException catch (e) {
+ debugPrint('[VoiceRoomForeground] stop failed: ${e.message}');
+ } catch (e) {
+ debugPrint('[VoiceRoomForeground] stop failed: $e');
+ }
+ }
+}
diff --git a/lib/shared/data_sources/sources/local/floating_screen_manager.dart b/lib/shared/data_sources/sources/local/floating_screen_manager.dart
index d19145a..0792c5e 100644
--- a/lib/shared/data_sources/sources/local/floating_screen_manager.dart
+++ b/lib/shared/data_sources/sources/local/floating_screen_manager.dart
@@ -24,12 +24,14 @@ typedef FloatingScreenManager = OverlayManager;
class OverlayManager {
static const int _maxLowPerformanceCompensationQueueLength = 8;
+ static const int _maxDeferredRegionRedPacketCount = 12;
static const Duration _compensationDedupWindow = Duration(seconds: 3);
static const Duration _redPacketFloatingDedupWindow = Duration(minutes: 30);
final SCPriorityQueue _messageQueue = SCPriorityQueue(
(a, b) => b.priority.compareTo(a.priority),
);
+ final List _deferredRegionRedPacketMessages = [];
final Map _recentCompensationKeys = {};
final Map _recentFloatingKeys = {};
bool _isPlaying = false;
@@ -53,6 +55,12 @@ class OverlayManager {
if (_isRecentFloatingDuplicate(message)) {
return;
}
+ if (message.type == 4) {
+ debugPrint(
+ '[RoomRedPacket][Floating] enqueue packetId=${message.toUserId} '
+ 'roomId=${message.roomId} region=${message.isRegionBroadcast}',
+ );
+ }
_rememberFloatingMessage(message);
_enqueueMessage(
message,
@@ -61,6 +69,7 @@ class OverlayManager {
} else {
_removeActiveMessage(scheduleNext: false);
_messageQueue.clear();
+ _deferredRegionRedPacketMessages.clear();
_isPlaying = false;
_isProcessing = false;
_isDisposed = false;
@@ -256,9 +265,20 @@ class OverlayManager {
// 安全地获取第一个消息
if (_messageQueue.isEmpty) return;
final messageToProcess = _messageQueue.first;
+ if (messageToProcess == null) {
+ _messageQueue.removeFirst();
+ _safeScheduleNext();
+ return;
+ }
if (!_shouldDisplayMessage(context, messageToProcess)) {
_messageQueue.removeFirst();
+ if (_shouldDeferRegionRedPacketMessage(context, messageToProcess)) {
+ _deferRegionRedPacketMessage(context, messageToProcess);
+ _safeScheduleNext();
+ return;
+ }
+ _logRegionRedPacketDrop(context, messageToProcess);
_safeScheduleNext();
return;
}
@@ -401,6 +421,7 @@ class OverlayManager {
_removeRegionBroadcastMessages();
return;
}
+ _restoreDeferredRegionRedPacketMessages();
_safeScheduleNext();
}
@@ -434,8 +455,10 @@ class OverlayManager {
if (SCGlobalConfig.isFloatingBroadcastClosed) {
_removeActiveMessage(scheduleNext: false);
_messageQueue.clear();
+ _deferredRegionRedPacketMessages.clear();
return;
}
+ _restoreDeferredRegionRedPacketMessages();
final context = navigatorKey.currentState?.context;
final activeMessage = _currentMessage;
if (context != null &&
@@ -456,6 +479,7 @@ class OverlayManager {
_currentOverlayEntry = null;
_currentMessage = null;
_messageQueue.clear();
+ _deferredRegionRedPacketMessages.clear();
_recentCompensationKeys.clear();
_isPlaying = false;
_isProcessing = false;
@@ -497,6 +521,86 @@ class OverlayManager {
bool get _isSuppressed => _suppressedCount > 0;
+ bool _isRegionRedPacketMessage(SCFloatingMessage message) {
+ return message.type == 4 && message.isRegionBroadcast;
+ }
+
+ bool _shouldDeferRegionRedPacketMessage(
+ BuildContext context,
+ SCFloatingMessage message,
+ ) {
+ if (!_isRegionRedPacketMessage(message) ||
+ SCGlobalConfig.isFloatingBroadcastClosed) {
+ return false;
+ }
+ return !_shouldDisplayRegionBroadcastMessage(context, message);
+ }
+
+ void _deferRegionRedPacketMessage(
+ BuildContext context,
+ SCFloatingMessage message,
+ ) {
+ final key = _floatingDedupKeyFor(message);
+ if (key != null &&
+ _deferredRegionRedPacketMessages.any(
+ (item) => _floatingDedupKeyFor(item) == key,
+ )) {
+ return;
+ }
+ while (_deferredRegionRedPacketMessages.length >=
+ _maxDeferredRegionRedPacketCount) {
+ _deferredRegionRedPacketMessages.removeAt(0);
+ }
+ _deferredRegionRedPacketMessages.add(message);
+ debugPrint(
+ '[RoomRedPacket][Floating] defer packetId=${message.toUserId} '
+ 'roomId=${message.roomId} ${_displayStateForLog(context)}',
+ );
+ }
+
+ void _restoreDeferredRegionRedPacketMessages() {
+ if (_deferredRegionRedPacketMessages.isEmpty) {
+ return;
+ }
+ final messages = List.from(
+ _deferredRegionRedPacketMessages,
+ );
+ _deferredRegionRedPacketMessages.clear();
+ for (final message in messages) {
+ _messageQueue.add(message);
+ }
+ debugPrint(
+ '[RoomRedPacket][Floating] restore deferred count=${messages.length}',
+ );
+ }
+
+ void _logRegionRedPacketDrop(
+ BuildContext context,
+ SCFloatingMessage message,
+ ) {
+ if (message.type != 4) {
+ return;
+ }
+ debugPrint(
+ '[RoomRedPacket][Floating] drop packetId=${message.toUserId} '
+ 'roomId=${message.roomId} region=${message.isRegionBroadcast} '
+ '${_displayStateForLog(context)}',
+ );
+ }
+
+ String _displayStateForLog(BuildContext context) {
+ final rtcProvider = Provider.of(
+ context,
+ listen: false,
+ );
+ final currentRoomId =
+ rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? "";
+ return 'homeVisible=$_homeRootTabsVisible '
+ 'roomVisible=${rtcProvider.shouldShowRoomVisualEffects} '
+ 'currentRoomId=$currentRoomId '
+ 'scope=${SCGlobalConfig.floatingBroadcastScope}';
+ }
+
bool _shouldDisplayMessage(BuildContext context, SCFloatingMessage? message) {
if (message == null) {
return false;
@@ -582,7 +686,7 @@ class OverlayManager {
while (_messageQueue.isNotEmpty) {
final message = _messageQueue.removeFirst();
- if (!message.isRegionBroadcast) {
+ if (!message.isRegionBroadcast || _isRegionRedPacketMessage(message)) {
newQueue.add(message);
}
}
diff --git a/lib/ui_kit/widgets/room/red_packet/room_red_packet_send_panel.dart b/lib/ui_kit/widgets/room/red_packet/room_red_packet_send_panel.dart
index 2bdb475..9dea6ef 100644
--- a/lib/ui_kit/widgets/room/red_packet/room_red_packet_send_panel.dart
+++ b/lib/ui_kit/widgets/room/red_packet/room_red_packet_send_panel.dart
@@ -607,6 +607,11 @@ class _RoomRedPacketSendPanelState extends State {
(_selectedPickupMinutes > 0 ? _selectedPickupMinutes * 60 : 0)
..['claimStartTime'] = claimStartTime
..['grabbed'] = result.grabbed ?? false;
+ final roomRegionCode = rtcProvider.currenRoom?.roomProfile?.regionCode;
+ if ((payload['regionCode']?.toString().trim() ?? '').isEmpty &&
+ (roomRegionCode?.trim().isNotEmpty ?? false)) {
+ payload['regionCode'] = roomRegionCode!.trim();
+ }
final rtmProvider = Provider.of(context, listen: false);
rtmProvider.addMsg(
diff --git a/lib/ui_kit/widgets/room/room_play_widget.dart b/lib/ui_kit/widgets/room/room_play_widget.dart
index 32667c0..3d30818 100644
--- a/lib/ui_kit/widgets/room/room_play_widget.dart
+++ b/lib/ui_kit/widgets/room/room_play_widget.dart
@@ -68,6 +68,7 @@ class _RoomRedPacketFloatingEntryState
Timer? _entryRefreshTimer;
bool _refreshingList = false;
int _elapsedSeconds = 0;
+ String _observedRoomId = '';
String _verifiedRoomId = '';
int _verifiedRoomRefreshCount = 0;
Set _verifiedKnownPacketIds = const {};
@@ -80,6 +81,11 @@ class _RoomRedPacketFloatingEntryState
const Duration(seconds: 1),
(_) => _onEntryTimerTick(),
);
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (mounted) {
+ _refreshRoomRedPacketList();
+ }
+ });
}
@override
@@ -93,6 +99,7 @@ class _RoomRedPacketFloatingEntryState
if (!mounted) {
return;
}
+ _refreshRoomRedPacketListIfRoomChanged();
setState(() {});
_elapsedSeconds++;
if (_elapsedSeconds % _listRefreshIntervalSeconds == 0) {
@@ -100,6 +107,24 @@ class _RoomRedPacketFloatingEntryState
}
}
+ void _refreshRoomRedPacketListIfRoomChanged() {
+ try {
+ final rtcProvider = Provider.of(context, listen: false);
+ final currentRoomId =
+ rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? '';
+ if (currentRoomId.isEmpty || currentRoomId == _observedRoomId) {
+ return;
+ }
+ _observedRoomId = currentRoomId;
+ _elapsedSeconds = 0;
+ _verifiedRoomId = '';
+ _verifiedRoomRefreshCount = 0;
+ _verifiedKnownPacketIds = const {};
+ _verifiedVisiblePacketIds = const {};
+ _refreshRoomRedPacketList();
+ } catch (_) {}
+ }
+
void _refreshRoomRedPacketList() {
if (_refreshingList || !mounted) {
return;
@@ -116,6 +141,13 @@ class _RoomRedPacketFloatingEntryState
rtcProvider
.loadRoomRedPacketList(1)
.then((result) {
+ final activeRoomId =
+ rtcProvider.currenRoom?.roomProfile?.roomProfile?.id
+ ?.trim() ??
+ '';
+ if (activeRoomId != currentRoomId) {
+ return result;
+ }
final serverPackets =
result.map(RoomRedPacketUiData.fromListRes).toList();
final serverPacketIds =
diff --git a/pubspec.yaml b/pubspec.yaml
index 7ddbebe..a60a2f8 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
-version: 1.3.0+10
+version: 1.3.0+11
environment:
diff --git a/需求进度.md b/需求进度.md
index 29fcae4..ecd7676 100644
--- a/需求进度.md
+++ b/需求进度.md
@@ -562,6 +562,7 @@
- 已修正红包领取成功公屏消息的多语言问题:领取后不再发送已翻译好的普通文本,改为 `ROOM_RED_PACKET_CLAIM` 结构化房间 IM,只传领取金额、红包发送人名和 `packetId`,公屏渲染时按当前客户端语言生成文案,避免英文用户看到阿语消息。
- 已补强房外延时红包飘屏和领取通知去重:首页四个一级 Tab 可见/回到前台时会重新同步当前语区红包 IM 群;语区红包飘屏在首页 Tab 中不再受“只看房间飘屏”设置影响;领取成功公屏通知按 `packetId + 领取人ID` 生成稳定去重键,避免本地插入和 IM 回包/历史拉取造成两条一模一样的领取消息。
- 已定位并修复公屏上滑历史反复弹 `user info not found`:历史消息 item 重建时会重复调用用户资料接口,缺失用户返回错误后被网络层 toast;现在房间公屏补拉用户资料改为静默请求,并对同一 userId 的请求/失败结果做缓存,5 分钟内不再反复请求同一个不存在用户。
+- 已继续排查房外/跨房红包飘屏缺失:IM 重登成功后会立即重新同步当前语区红包群;收到未登记但内容为 `ROOM_RED_PACKET` 的群消息时会按语区红包飘屏兜底处理,避免语区群 id 尚未写入时被普通群消息分支吞掉;区域红包在首页 Tab 暂不可见时不再直接从飘屏队列丢弃,而是挂起后在回到首页 Tab 时恢复播放;发送红包 payload 也会补齐当前房间 `regionCode`,便于日志核对和前端兜底过滤。
## 已知问题
- 已接入语音房表情包素材第一版:桌面 `表情素材` 中的 `fluent_emoji` 与 `monkey` gif 已纳入项目资源,底部聊天入口改为图稿里的短白色胶囊并移除内部输入图标,右侧新增独立表情按钮;同一个房间输入弹层现在可从表情按钮直接打开表情包面板,原 unicode emoji 面板已替换为本地 gif 表情。发送逻辑同步按最新规则分流:用户在麦上发表情时只在自己麦位播放,不进入聊天列表;用户未上麦发表情时进入房间聊天框展示,不触发麦位动画。