From 8ac9590a8fcafd2be13d06d47e6ea5aa08f0dd8e Mon Sep 17 00:00:00 2001 From: roxy Date: Mon, 8 Jun 2026 10:16:56 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8E=9F=E7=94=9F=E8=A7=A3=E6=9E=90=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/yumiparty/GiftMp4VideoPlatformView.kt | 20 ++++- lib/modules/room/voice_room_page.dart | 67 +++++++-------- lib/services/audio/rtc_manager.dart | 85 ++++++++++++++----- lib/services/audio/rtm_manager.dart | 15 ++-- .../local/floating_screen_manager.dart | 23 +++-- .../tools/sc_gift_vap_svga_manager.dart | 70 ++++++++++++++- .../room/effect/gift_mp4_effect_view.dart | 58 ++++++++++++- 7 files changed, 258 insertions(+), 80 deletions(-) diff --git a/android/app/src/main/kotlin/com/org/yumiparty/GiftMp4VideoPlatformView.kt b/android/app/src/main/kotlin/com/org/yumiparty/GiftMp4VideoPlatformView.kt index cd44487..a616159 100644 --- a/android/app/src/main/kotlin/com/org/yumiparty/GiftMp4VideoPlatformView.kt +++ b/android/app/src/main/kotlin/com/org/yumiparty/GiftMp4VideoPlatformView.kt @@ -132,6 +132,8 @@ class GiftMp4VideoPlatformView( } } +private const val GIFT_MP4_VAPC_SCAN_BYTES = 512 * 1024 + private class GiftMp4AlphaTextureView(context: Context) : TextureView(context), TextureView.SurfaceTextureListener { @@ -826,7 +828,7 @@ private data class GiftMp4NativeLayout( val marker = "vapc".toByteArray(Charsets.US_ASCII) val markerIndex = bytes.indexOf(marker, 0) if (markerIndex < 0) return null - val searchEnd = min(bytes.size, markerIndex + marker.size + 512 * 1024) + val searchEnd = min(bytes.size, markerIndex + marker.size + GIFT_MP4_VAPC_SCAN_BYTES) for (index in markerIndex + marker.size until searchEnd) { if (bytes[index] != '{'.code.toByte()) continue val end = findJsonObjectEnd(bytes, index, searchEnd) @@ -991,7 +993,21 @@ private data class GiftMp4ResolvedSource( fun readBytes(): ByteArray? = runCatching { - file.readBytes() + file.inputStream().use { input -> + val readLength = min(file.length(), GIFT_MP4_VAPC_SCAN_BYTES.toLong()).toInt() + if (readLength <= 0) { + return@use ByteArray(0) + } + val buffer = ByteArray(readLength) + val bytesRead = input.read(buffer) + if (bytesRead <= 0) { + ByteArray(0) + } else if (bytesRead == buffer.size) { + buffer + } else { + buffer.copyOf(bytesRead) + } + } }.getOrNull() fun close() = Unit diff --git a/lib/modules/room/voice_room_page.dart b/lib/modules/room/voice_room_page.dart index 2f85c93..8458c9a 100644 --- a/lib/modules/room/voice_room_page.dart +++ b/lib/modules/room/voice_room_page.dart @@ -727,43 +727,38 @@ class _VoiceRoomPageState extends State targetGroupKey == null || _markGiftVisualBatchForPlayback(msg, targetGroupKey); if (shouldPlayBatchVisuals) { - SCRoomEffectScheduler().scheduleDeferredEffect( - debugLabel: 'room_gift_ticker_${msg.type}_${msg.gift?.id ?? ""}', - action: () { - if (!mounted) { - return; - } - var giftModel = LGiftModel(); - giftModel.labelId = _buildGiftTickerLabelId(msg, targetGroupKey); - giftModel.sendUserName = msg.user?.userNickname ?? ""; - giftModel.sendToUserName = _resolveGiftTickerTargetName( - msg, - targetUserIds, - ); - giftModel.sendUserPic = msg.user?.userAvatar ?? ""; - giftModel.giftPic = msg.gift?.giftPhoto ?? ""; - giftModel.giftCount = msg.number ?? 0; - giftModel.giftCountStepUnit = _resolveGiftCountStepUnit(msg); - unawaited( - warmImageResource( - giftModel.sendUserPic, - logicalWidth: 26.w, - logicalHeight: 26.w, - ), - ); - unawaited( - warmImageResource( - giftModel.giftPic, - logicalWidth: 34.w, - logicalHeight: 34.w, - ), - ); - Provider.of( - context, - listen: false, - ).enqueueGiftAnimation(giftModel); - }, + if (!mounted) { + return; + } + var giftModel = LGiftModel(); + giftModel.labelId = _buildGiftTickerLabelId(msg, targetGroupKey); + giftModel.sendUserName = msg.user?.userNickname ?? ""; + giftModel.sendToUserName = _resolveGiftTickerTargetName( + msg, + targetUserIds, ); + giftModel.sendUserPic = msg.user?.userAvatar ?? ""; + giftModel.giftPic = msg.gift?.giftPhoto ?? ""; + giftModel.giftCount = msg.number ?? 0; + giftModel.giftCountStepUnit = _resolveGiftCountStepUnit(msg); + unawaited( + warmImageResource( + giftModel.sendUserPic, + logicalWidth: 26.w, + logicalHeight: 26.w, + ), + ); + unawaited( + warmImageResource( + giftModel.giftPic, + logicalWidth: 34.w, + logicalHeight: 34.w, + ), + ); + Provider.of( + context, + listen: false, + ).enqueueGiftAnimation(giftModel); } else { debugPrint( '[GiftFx][voice] skip gift ticker by visual batch dedupe ' diff --git a/lib/services/audio/rtc_manager.dart b/lib/services/audio/rtc_manager.dart index 78408cc..296fc90 100644 --- a/lib/services/audio/rtc_manager.dart +++ b/lib/services/audio/rtc_manager.dart @@ -338,11 +338,9 @@ class _RecentMicChangeSeatSnapshot { class RealTimeCommunicationManager extends ChangeNotifier { static const String _roomRocketPagLaunchDialogTag = 'showRoomRocketPagLaunchDialog'; - static const List _roomRocketGiftRefreshDelays = [ - Duration(milliseconds: 700), - Duration(milliseconds: 1600), - Duration(milliseconds: 3200), - ]; + static const Duration _roomRocketGiftRefreshThrottleDelay = Duration( + milliseconds: 700, + ); static const List _roomRocketPostLaunchRefreshDelays = [ Duration(milliseconds: 800), Duration(seconds: 2), @@ -407,7 +405,10 @@ class RealTimeCommunicationManager extends ChangeNotifier { Timer? _roomEntryEffectTimer; Timer? _roomRocketLaunchAnimationTimer; ValueNotifier? _roomRocketLaunchTopAvatarNotifier; - final List _roomRocketGiftRefreshTimers = []; + Timer? _roomRocketGiftRefreshTimer; + String? _roomRocketGiftRefreshRoomId; + bool _roomRocketGiftRefreshDirty = false; + bool _roomRocketGiftRefreshInFlight = false; final List _roomRocketPostLaunchRefreshTimers = []; final List _roomRocketEntryReadyFallbackTimers = []; Timer? _roomRedPacketPresenceTimer; @@ -4122,25 +4123,63 @@ class RealTimeCommunicationManager extends ChangeNotifier { if (resolvedRoomId.isEmpty) { return; } - _cancelRoomRocketGiftStatusRefresh(); - for (final delay in _roomRocketGiftRefreshDelays) { - final timer = Timer(delay, () { - _roomRocketGiftRefreshTimers.removeWhere((item) => !item.isActive); - if (resolvedRoomId != - (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim()) { - return; - } - unawaited(refreshRoomRocketStatus(roomId: resolvedRoomId)); - }); - _roomRocketGiftRefreshTimers.add(timer); + _roomRocketGiftRefreshRoomId = resolvedRoomId; + _roomRocketGiftRefreshDirty = true; + if (_roomRocketGiftRefreshTimer != null || _roomRocketGiftRefreshInFlight) { + return; + } + _roomRocketGiftRefreshTimer = Timer( + _roomRocketGiftRefreshThrottleDelay, + () => unawaited(_runRoomRocketGiftStatusRefresh()), + ); + } + + void _schedulePendingRoomRocketGiftStatusRefreshIfNeeded() { + if (!_roomRocketGiftRefreshDirty || _roomRocketGiftRefreshTimer != null) { + return; + } + final pendingRoomId = (_roomRocketGiftRefreshRoomId ?? "").trim(); + final currentRoomId = + (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(); + if (pendingRoomId.isEmpty || pendingRoomId != currentRoomId) { + return; + } + _roomRocketGiftRefreshTimer = Timer( + _roomRocketGiftRefreshThrottleDelay, + () => unawaited(_runRoomRocketGiftStatusRefresh()), + ); + } + + Future _runRoomRocketGiftStatusRefresh() async { + _roomRocketGiftRefreshTimer = null; + if (_roomRocketGiftRefreshInFlight) { + return; + } + final resolvedRoomId = (_roomRocketGiftRefreshRoomId ?? "").trim(); + if (resolvedRoomId.isEmpty) { + _roomRocketGiftRefreshDirty = false; + return; + } + _roomRocketGiftRefreshDirty = false; + if (resolvedRoomId != + (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim()) { + _schedulePendingRoomRocketGiftStatusRefreshIfNeeded(); + return; + } + _roomRocketGiftRefreshInFlight = true; + try { + await refreshRoomRocketStatus(roomId: resolvedRoomId); + } finally { + _roomRocketGiftRefreshInFlight = false; + _schedulePendingRoomRocketGiftStatusRefreshIfNeeded(); } } void _cancelRoomRocketGiftStatusRefresh() { - for (final timer in _roomRocketGiftRefreshTimers) { - timer.cancel(); - } - _roomRocketGiftRefreshTimers.clear(); + _roomRocketGiftRefreshTimer?.cancel(); + _roomRocketGiftRefreshTimer = null; + _roomRocketGiftRefreshRoomId = null; + _roomRocketGiftRefreshDirty = false; } void _scheduleRoomRocketEntryReadyFallback(String roomId) { @@ -4384,8 +4423,8 @@ class RealTimeCommunicationManager extends ChangeNotifier { } SCGiftVapSvgaManager().play( animationUrl, - priority: SCGiftVapSvgaManager.entryEffectPriority + 100, - type: SCGiftVapSvgaManager.entryEffectType, + priority: SCGiftVapSvgaManager.rocketLaunchEffectPriority, + type: SCGiftVapSvgaManager.rocketLaunchEffectType, ); } diff --git a/lib/services/audio/rtm_manager.dart b/lib/services/audio/rtm_manager.dart index 90e919d..417de03 100644 --- a/lib/services/audio/rtm_manager.dart +++ b/lib/services/audio/rtm_manager.dart @@ -5149,8 +5149,6 @@ class RealTimeMessagingManager extends ChangeNotifier { RoomRocketLaunchBroadcastMessage launch, ) { _clearRoomRocketLaunchCountdownForRoom(launch.roomId); - _pendingRoomRocketRewardPopups.remove(launch.roomId); - _recentRoomRocketRewardProbeTimes.remove(launch.roomId); _markRoomRocketLaunchEffectConsumed(launch); } @@ -6111,10 +6109,15 @@ class RealTimeMessagingManager extends ChangeNotifier { currentContext, listen: false, ); - rtcProvider.advanceRoomRocketStatusAfterLaunch(launch); - rtcProvider.scheduleRoomRocketPostLaunchStatusRefresh( - roomId: launchRoomId, - ); + if (_isCurrentVisibleVoiceRoom(launchRoomId)) { + _markRoomRocketRewardProbe(launch.roomId); + _playRoomRocketLaunchEffectsAndReward(launch); + } else { + rtcProvider.advanceRoomRocketStatusAfterLaunch(launch); + rtcProvider.scheduleRoomRocketPostLaunchStatusRefresh( + roomId: launchRoomId, + ); + } } } _scheduleRoomRocketStatusRefreshForCurrentRoom(launchRoomId); 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 c075317..d663295 100644 --- a/lib/shared/data_sources/sources/local/floating_screen_manager.dart +++ b/lib/shared/data_sources/sources/local/floating_screen_manager.dart @@ -85,16 +85,23 @@ class OverlayManager { SCFloatingMessage message, { required String debugLabel, }) { + void enqueueNow() { + if (_isDisposed || _isSuppressed) { + return; + } + unawaited(_warmMessageImages(message)); + _messageQueue.add(message); + _safeScheduleNext(); + } + + if (message.type == 1) { + enqueueNow(); + return; + } + SCRoomEffectScheduler().scheduleDeferredEffect( debugLabel: debugLabel, - action: () { - if (_isDisposed || _isSuppressed) { - return; - } - unawaited(_warmMessageImages(message)); - _messageQueue.add(message); - _safeScheduleNext(); - }, + action: enqueueNow, ); } diff --git a/lib/shared/tools/sc_gift_vap_svga_manager.dart b/lib/shared/tools/sc_gift_vap_svga_manager.dart index 526bc98..5a2e902 100644 --- a/lib/shared/tools/sc_gift_vap_svga_manager.dart +++ b/lib/shared/tools/sc_gift_vap_svga_manager.dart @@ -16,10 +16,13 @@ import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; class SCGiftVapSvgaManager { static const int giftEffectType = 0; static const int entryEffectType = 1; + static const int rocketLaunchEffectType = 2; static const int giftEffectPriority = 100; static const int entryEffectPriority = 1000; + static const int rocketLaunchEffectPriority = 2000; static const int _maxPendingGiftTaskCount = 24; static const int _maxPendingEntryTaskCount = 5; + static const int _maxPendingRocketLaunchTaskCount = 3; static const int _maxConsecutiveEntryTasksBeforeGift = 2; static const Duration _taskWatchdogTimeout = Duration(seconds: 20); static const Duration _entryTaskTtl = Duration(seconds: 6); @@ -40,6 +43,10 @@ class SCGiftVapSvgaManager { final SCPriorityQueue _entryQueue = SCPriorityQueue( (a, b) => a.compareTo(b), // 调用 SCVapTask 的 compareTo 方法 ); + final SCPriorityQueue _rocketLaunchQueue = + SCPriorityQueue( + (a, b) => a.compareTo(b), // 调用 SCVapTask 的 compareTo 方法 + ); SCGiftMp4Controller? _mp4Controller; SVGAAnimationController? _rsc; bool _play = false; @@ -97,6 +104,11 @@ class SCGiftVapSvgaManager { if (type == entryEffectType) { return priority > entryEffectPriority ? priority : entryEffectPriority; } + if (type == rocketLaunchEffectType) { + return priority > rocketLaunchEffectPriority + ? priority + : rocketLaunchEffectPriority; + } if (priority != 0) { return priority; } @@ -105,10 +117,15 @@ class SCGiftVapSvgaManager { bool _isEntryTask(SCVapTask task) => task.type == entryEffectType; - int get _pendingTaskCount => _giftQueue.length + _entryQueue.length; + bool _isRocketLaunchTask(SCVapTask task) => + task.type == rocketLaunchEffectType; + + int get _pendingTaskCount => + _giftQueue.length + _entryQueue.length + _rocketLaunchQueue.length; String get _queueSummary => - 'entry=${_entryQueue.length} gift=${_giftQueue.length}'; + 'rocket=${_rocketLaunchQueue.length} entry=${_entryQueue.length} ' + 'gift=${_giftQueue.length}'; bool _isControllerReady(SCVapTask task) { if (_needsSvgaController(task.path)) { @@ -204,7 +221,19 @@ class SCGiftVapSvgaManager { await _loadSvgaEntity(path); return; } - await _ensurePlayableFilePath(path); + final pathType = SCPathUtils.getPathType(path); + if (pathType == PathType.asset) { + await SCGiftMp4Controller.preloadVapLayout( + path: path, + sourceType: SCGiftMp4SourceType.asset, + ); + return; + } + final playablePath = await _ensurePlayableFilePath(path); + await SCGiftMp4Controller.preloadVapLayout( + path: playablePath, + sourceType: SCGiftMp4SourceType.file, + ); } Future _loadSvgaEntity(String path) async { @@ -328,6 +357,11 @@ class SCGiftVapSvgaManager { } void _enqueueTask(SCVapTask task) { + if (_isRocketLaunchTask(task)) { + _rocketLaunchQueue.add(task); + _trimRocketLaunchQueue(); + return; + } if (_isEntryTask(task)) { _dropExpiredEntryTasks(); _entryQueue.add(task); @@ -338,6 +372,27 @@ class SCGiftVapSvgaManager { _trimGiftQueue(); } + void _trimRocketLaunchQueue() { + while (_rocketLaunchQueue.length > _maxPendingRocketLaunchTaskCount) { + final removedTask = _removeRocketLaunchOverflowTask(); + _completeDroppedQueuedTask(removedTask, 'trim rocket launch queue'); + } + } + + SCVapTask _removeRocketLaunchOverflowTask() { + final tasks = _rocketLaunchQueue.unorderedElements.cast(); + var candidate = tasks.first; + for (final task in tasks.skip(1)) { + if (task.priority < candidate.priority || + (task.priority == candidate.priority && + task.createdAt.isBefore(candidate.createdAt))) { + candidate = task; + } + } + _rocketLaunchQueue.remove(candidate); + return candidate; + } + void _trimEntryQueue() { while (_entryQueue.length > _maxPendingEntryTaskCount) { final removedTask = _removeEntryOverflowTask(); @@ -394,6 +449,9 @@ class SCGiftVapSvgaManager { } SCVapTask? _peekNextQueuedTask() { + if (_rocketLaunchQueue.isNotEmpty) { + return _rocketLaunchQueue.first; + } _dropExpiredEntryTasks(); if (_entryQueue.isNotEmpty) { final shouldYieldToGift = @@ -408,6 +466,11 @@ class SCGiftVapSvgaManager { } void _removeQueuedTask(SCVapTask task) { + if (_isRocketLaunchTask(task)) { + _rocketLaunchQueue.remove(task); + _consecutiveEntryTaskCount = 0; + return; + } if (_isEntryTask(task)) { _entryQueue.remove(task); _consecutiveEntryTaskCount++; @@ -755,6 +818,7 @@ class SCGiftVapSvgaManager { _cancelCurrentTaskWatchdog(); _giftQueue.clear(); _entryQueue.clear(); + _rocketLaunchQueue.clear(); _preloadQueue.clear(); _queuedPreloadPaths.clear(); _activePreloadCount = 0; diff --git a/lib/ui_kit/widgets/room/effect/gift_mp4_effect_view.dart b/lib/ui_kit/widgets/room/effect/gift_mp4_effect_view.dart index 586f6f7..2dae910 100644 --- a/lib/ui_kit/widgets/room/effect/gift_mp4_effect_view.dart +++ b/lib/ui_kit/widgets/room/effect/gift_mp4_effect_view.dart @@ -53,6 +53,13 @@ class SCGiftMp4Controller extends ChangeNotifier { SCGiftMp4PlayRequest? get request => _request; + static Future preloadVapLayout({ + required String path, + required SCGiftMp4SourceType sourceType, + }) async { + await _parseVapLayout(path: path, sourceType: sourceType); + } + static void _installEventsHandler() { if (_eventsHandlerInstalled) { return; @@ -331,15 +338,44 @@ class _GiftMp4EffectViewState extends State { } } +const int _vapLayoutScanBytes = 512 * 1024; +final Map?> _vapLayoutCache = + ?>{}; +final Map?>> _vapLayoutTasks = + ?>>{}; + Future?> _parseVapLayout({ required String path, required SCGiftMp4SourceType sourceType, +}) async { + final cacheKey = '${sourceType.name}|$path'; + if (_vapLayoutCache.containsKey(cacheKey)) { + return _vapLayoutCache[cacheKey]; + } + final loadingTask = _vapLayoutTasks[cacheKey]; + if (loadingTask != null) { + return loadingTask; + } + final future = _parseVapLayoutUncached(path: path, sourceType: sourceType); + _vapLayoutTasks[cacheKey] = future; + try { + final layout = await future; + _vapLayoutCache[cacheKey] = layout; + return layout; + } finally { + _vapLayoutTasks.remove(cacheKey); + } +} + +Future?> _parseVapLayoutUncached({ + required String path, + required SCGiftMp4SourceType sourceType, }) async { try { final bytes = sourceType == SCGiftMp4SourceType.asset ? (await rootBundle.load(path)).buffer.asUint8List() - : await File(path.removePrefix('file://')).readAsBytes(); + : await _readFilePrefixBytes(path.removePrefix('file://')); final jsonText = _extractVapcJson(bytes); if (jsonText == null) { return null; @@ -382,6 +418,22 @@ Future?> _parseVapLayout({ } } +Future _readFilePrefixBytes(String path) async { + final file = File(path); + final length = await file.length(); + final readLength = + length < _vapLayoutScanBytes ? length.toInt() : _vapLayoutScanBytes; + if (readLength <= 0) { + return Uint8List(0); + } + final randomAccessFile = await file.open(); + try { + return await randomAccessFile.read(readLength); + } finally { + await randomAccessFile.close(); + } +} + String? _extractVapcJson(Uint8List bytes) { const marker = [0x76, 0x61, 0x70, 0x63]; // vapc final markerIndex = _indexOfBytes(bytes, marker, 0); @@ -389,7 +441,9 @@ String? _extractVapcJson(Uint8List bytes) { return null; } final searchEnd = - (markerIndex + marker.length + 512 * 1024).clamp(0, bytes.length).toInt(); + (markerIndex + marker.length + _vapLayoutScanBytes) + .clamp(0, bytes.length) + .toInt(); for (var index = markerIndex + marker.length; index < searchEnd; index++) { if (bytes[index] != 0x7b) { continue;