import 'dart:async'; import 'dart:collection'; import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:flutter_svga/flutter_svga.dart'; import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/shared/tools/sc_path_utils.dart'; import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart'; import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart'; import 'package:tancent_vap/utils/constant.dart'; import 'package:yumi/ui_kit/widgets/room/effect/gift_mp4_effect_view.dart'; 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 giftEffectPriority = 100; static const int entryEffectPriority = 1000; static const int _maxPendingGiftTaskCount = 24; static const int _maxPendingEntryTaskCount = 5; static const int _maxConsecutiveEntryTasksBeforeGift = 2; static const Duration _taskWatchdogTimeout = Duration(seconds: 20); static const Duration _entryTaskTtl = Duration(seconds: 6); Map videoItemCache = {}; static SCGiftVapSvgaManager? _inst; static const int _maxPreloadConcurrency = 1; static const int _maxPreloadQueueLength = 12; static const int _maxSvgaCacheEntries = 8; static const int _maxPlayablePathCacheEntries = 24; SCGiftVapSvgaManager._internal(); factory SCGiftVapSvgaManager() => _inst ??= SCGiftVapSvgaManager._internal(); final SCPriorityQueue _giftQueue = SCPriorityQueue( (a, b) => a.compareTo(b), // 调用 SCVapTask 的 compareTo 方法 ); final SCPriorityQueue _entryQueue = SCPriorityQueue( (a, b) => a.compareTo(b), // 调用 SCVapTask 的 compareTo 方法 ); SCGiftMp4Controller? _mp4Controller; SVGAAnimationController? _rsc; bool _play = false; bool _dis = false; SCVapTask? _currentTask; Timer? _currentTaskWatchdogTimer; final Queue _preloadQueue = Queue(); final Set _queuedPreloadPaths = {}; final Map> _svgaLoadTasks = {}; final Map> _playablePathTasks = {}; final Map _playablePathCache = {}; int _activePreloadCount = 0; int _consecutiveEntryTaskCount = 0; bool _pause = false; //是否关闭礼物特效声音 bool _mute = false; void setMute(bool muteMusic) { _mute = muteMusic; _rsc?.muted = _mute; final mp4Controller = _mp4Controller; if (mp4Controller != null) { unawaited(mp4Controller.setMute(_mute)); } unawaited(DataPersistence.setPlayGiftMusic(_mute)); } bool getMute() { return _mute; } void _log(String message) { debugPrint('[GiftFx][manager] $message'); } bool _needsSvgaController(String path) { return SCPathUtils.getFileExtension(path).toLowerCase() == ".svga"; } bool _needsMp4Controller(String path) { final ext = SCPathUtils.getFileExtension(path).toLowerCase(); return ext == ".mp4" || ext == ".vap"; } bool _isSupportedEffectPath(String path) { return _needsSvgaController(path) || _needsMp4Controller(path); } static int resolveEffectPriority({ int priority = 0, int type = giftEffectType, }) { if (type == entryEffectType) { return priority > entryEffectPriority ? priority : entryEffectPriority; } if (priority != 0) { return priority; } return giftEffectPriority; } bool _isEntryTask(SCVapTask task) => task.type == entryEffectType; int get _pendingTaskCount => _giftQueue.length + _entryQueue.length; String get _queueSummary => 'entry=${_entryQueue.length} gift=${_giftQueue.length}'; bool _isControllerReady(SCVapTask task) { if (_needsSvgaController(task.path)) { return _rsc != null; } return _mp4Controller != null; } bool _isPlayableFileReady(String path) { final cachedPath = _playablePathCache[path]; if (cachedPath == null || cachedPath.isEmpty) { return false; } return File(cachedPath).existsSync(); } bool _isPreloadedOrLoading(String path) { if (_needsSvgaController(path)) { return videoItemCache.containsKey(path) || _svgaLoadTasks.containsKey(path); } final pathType = SCPathUtils.getPathType(path); if (pathType == PathType.asset || pathType == PathType.file) { return true; } return _isPlayableFileReady(path) || _playablePathTasks.containsKey(path); } Future preload(String path, {bool highPriority = false}) async { if (!SCGlobalConfig.allowsHighCostAnimations || path.isEmpty || _dis || _isPreloadedOrLoading(path)) { return; } if (!_isSupportedEffectPath(path)) { _log( 'preload ignored because effect file extension is unsupported ' 'path=$path ext=${SCPathUtils.getFileExtension(path)}', ); return; } if (highPriority) { _log('high priority preload path=$path'); await _warmupPath(path); return; } if (_queuedPreloadPaths.contains(path)) { return; } _trimPreloadQueue(); _preloadQueue.add(path); _queuedPreloadPaths.add(path); _log('enqueue preload path=$path queue=${_preloadQueue.length}'); _drainPreloadQueue(); } void _trimPreloadQueue() { while (_preloadQueue.length >= _maxPreloadQueueLength) { final removedPath = _preloadQueue.removeFirst(); _queuedPreloadPaths.remove(removedPath); } } void _drainPreloadQueue() { if (_dis || _pause || _play || _activePreloadCount >= _maxPreloadConcurrency || _preloadQueue.isEmpty) { return; } final path = _preloadQueue.removeFirst(); _queuedPreloadPaths.remove(path); _activePreloadCount++; _log( 'start preload path=$path active=$_activePreloadCount ' 'queueRemaining=${_preloadQueue.length}', ); _warmupPath(path).catchError((_) {}).whenComplete(() { _activePreloadCount = _activePreloadCount > 0 ? _activePreloadCount - 1 : 0; _log( 'finish preload path=$path active=$_activePreloadCount ' 'queueRemaining=${_preloadQueue.length}', ); _drainPreloadQueue(); }); } Future _warmupPath(String path) async { if (_needsSvgaController(path)) { await _loadSvgaEntity(path); return; } await _ensurePlayableFilePath(path); } Future _loadSvgaEntity(String path) async { final cached = _touchCachedSvgaEntity(path); if (cached != null) { return cached; } final loadingTask = _svgaLoadTasks[path]; if (loadingTask != null) { return loadingTask; } final future = () async { final pathType = SCPathUtils.getPathType(path); late final MovieEntity entity; if (pathType == PathType.asset) { entity = await SVGAParser.shared.decodeFromAssets(path); } else if (pathType == PathType.network) { entity = await SVGAParser.shared.decodeFromURL(path); } else if (pathType == PathType.file) { final bytes = await File(path).readAsBytes(); entity = await SVGAParser.shared.decodeFromBuffer(bytes); } else { throw Exception('Unsupported SVGA path: $path'); } entity.autorelease = false; if (!_dis) { _cacheSvgaEntity(path, entity); } return entity; }(); _svgaLoadTasks[path] = future; try { return await future; } finally { _svgaLoadTasks.remove(path); } } Future _ensurePlayableFilePath(String path) async { final pathType = SCPathUtils.getPathType(path); if (pathType == PathType.asset || pathType == PathType.file) { return path; } final cachedPath = _touchCachedPlayablePath(path); if (cachedPath != null && cachedPath.isNotEmpty && File(cachedPath).existsSync()) { return cachedPath; } final loadingTask = _playablePathTasks[path]; if (loadingTask != null) { return loadingTask; } final future = () async { final file = await FileCacheManager.getInstance().getFile(url: path); if (!_dis) { _cachePlayablePath(path, file.path); } return file.path; }(); _playablePathTasks[path] = future; try { return await future; } finally { _playablePathTasks.remove(path); } } MovieEntity? _touchCachedSvgaEntity(String path) { final cached = videoItemCache.remove(path); if (cached != null) { videoItemCache[path] = cached; } return cached; } void _cacheSvgaEntity(String path, MovieEntity entity) { final previous = videoItemCache.remove(path); if (previous != null && !identical(previous, entity)) { _disposeSvgaEntityIfUnused(previous); } videoItemCache[path] = entity; _trimSvgaCache(); } String? _touchCachedPlayablePath(String path) { final cachedPath = _playablePathCache.remove(path); if (cachedPath != null) { _playablePathCache[path] = cachedPath; } return cachedPath; } void _cachePlayablePath(String path, String playablePath) { _playablePathCache.remove(path); _playablePathCache[path] = playablePath; _trimResolvedCache(_playablePathCache, _maxPlayablePathCacheEntries); } void _trimSvgaCache() { while (videoItemCache.length > _maxSvgaCacheEntries) { final removableEntry = videoItemCache.entries.firstWhere( (entry) => !_isSvgaEntityInUse(entry.value), orElse: () => videoItemCache.entries.first, ); if (_isSvgaEntityInUse(removableEntry.value)) { videoItemCache.remove(removableEntry.key); videoItemCache[removableEntry.key] = removableEntry.value; break; } videoItemCache.remove(removableEntry.key); _disposeSvgaEntity(removableEntry.value); } } void _trimResolvedCache(Map cache, int maxEntries) { while (cache.length > maxEntries) { final oldestKey = cache.keys.first; cache.remove(oldestKey); } } void _enqueueTask(SCVapTask task) { if (_isEntryTask(task)) { _dropExpiredEntryTasks(); _entryQueue.add(task); _trimEntryQueue(); return; } _giftQueue.add(task); _trimGiftQueue(); } void _trimEntryQueue() { while (_entryQueue.length > _maxPendingEntryTaskCount) { final removedTask = _removeEntryOverflowTask(); _completeDroppedQueuedTask(removedTask, 'trim entry queue'); } } SCVapTask _removeEntryOverflowTask() { final tasks = _entryQueue.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; } } _entryQueue.remove(candidate); return candidate; } void _trimGiftQueue() { while (_giftQueue.length > _maxPendingGiftTaskCount) { final removedTask = _giftQueue.removeLast(); _completeDroppedQueuedTask(removedTask, 'trim gift queue'); } } void _dropExpiredEntryTasks() { if (_entryQueue.isEmpty) { return; } final now = DateTime.now(); final expiredTasks = _entryQueue.unorderedElements .cast() .where((task) => now.difference(task.createdAt) > _entryTaskTtl) .toList(); for (final task in expiredTasks) { _entryQueue.remove(task); _completeDroppedQueuedTask( task, 'drop expired entry task ageMs=${now.difference(task.createdAt).inMilliseconds}', ); } } void _completeDroppedQueuedTask(SCVapTask task, String reason) { SCRoomEffectScheduler().completeHighCostTask(debugLabel: task.path); _log( '$reason path=${task.path} priority=${task.priority} ' 'type=${task.type} queue=$_queueSummary', ); } SCVapTask? _peekNextQueuedTask() { _dropExpiredEntryTasks(); if (_entryQueue.isNotEmpty) { final shouldYieldToGift = _giftQueue.isNotEmpty && _consecutiveEntryTaskCount >= _maxConsecutiveEntryTasksBeforeGift; if (!shouldYieldToGift) { return _entryQueue.first; } return _giftQueue.first; } return _giftQueue.first; } void _removeQueuedTask(SCVapTask task) { if (_isEntryTask(task)) { _entryQueue.remove(task); _consecutiveEntryTaskCount++; return; } _giftQueue.remove(task); _consecutiveEntryTaskCount = 0; } void _scheduleNextTask({Duration delay = Duration.zero}) { if (_dis) { return; } if (delay == Duration.zero) { Future.microtask(_pn); return; } Future.delayed(delay, _pn); } void _finishCurrentTask({Duration delay = Duration.zero}) { if (_dis) { return; } final finishedTask = _currentTask; if (!_play && finishedTask == null) { _scheduleNextTask(delay: delay); return; } _cancelCurrentTaskWatchdog(); SCRoomEffectScheduler().completeHighCostTask( debugLabel: finishedTask?.path, ); _play = false; _currentTask = null; _scheduleNextTask(delay: delay); if (delay == Duration.zero) { Future.microtask(_drainPreloadQueue); } else { Future.delayed(delay, _drainPreloadQueue); } } // 绑定控制器 void bindMp4Ctrl(SCGiftMp4Controller mp4Controller) { _mute = DataPersistence.getPlayGiftMusic(); _dis = false; _mp4Controller = mp4Controller; unawaited(mp4Controller.setMute(_mute)); _log( 'bindMp4Ctrl hasMp4Ctrl=${_mp4Controller != null} ' 'hasSvgaCtrl=${_rsc != null} queue=$_queueSummary mute=$_mute', ); _mp4Controller?.setPlaybackListener( onStart: () { _log('mp4 onVideoStart path=${_currentTask?.path}'); }, onComplete: () { _log('mp4 onVideoComplete path=${_currentTask?.path}'); _hcs(); }, onFailed: (error) { _log('mp4 onFailed path=${_currentTask?.path} error=$error'); _hcs(); }, ); _scheduleNextTask(); _drainPreloadQueue(); } void bindSvgaCtrl(SVGAAnimationController svgaController) { _mute = DataPersistence.getPlayGiftMusic(); _dis = false; _rsc = svgaController; _rsc?.muted = _mute; _log( 'bindSvgaCtrl hasSvgaCtrl=${_rsc != null} ' 'hasMp4Ctrl=${_mp4Controller != null} queue=$_queueSummary', ); _rsc?.addStatusListener((AnimationStatus status) { if (status.isCompleted) { _log('svga completed path=${_currentTask?.path}'); _rsc?.reset(); _finishCurrentTask(); } }); _scheduleNextTask(); _drainPreloadQueue(); } // 播放任务 void play( String path, { int priority = 0, Map? customResources, int type = giftEffectType, }) { if (path.isEmpty) { _log('play ignored because path is empty'); return; } if (!_isSupportedEffectPath(path)) { _log( 'play ignored because effect file extension is unsupported ' 'path=$path ext=${SCPathUtils.getFileExtension(path)}', ); return; } if (SCRoomEffectScheduler().isSuppressed) { _log('play ignored because room effects are suppressed path=$path'); return; } final resolvedPriority = resolveEffectPriority( priority: priority, type: type, ); _log( 'play request path=$path ext=${SCPathUtils.getFileExtension(path)} ' 'priority=$priority resolvedPriority=$resolvedPriority type=$type ' 'sdkInt=${SCGlobalConfig.sdkInt} maxSdkNoAnim=${SCGlobalConfig.maxSdkNoAnim} ' 'lowPerformance=${SCGlobalConfig.isLowPerformanceDevice} ' 'disposed=$_dis playing=$_play queueBefore=$_queueSummary ' 'hasSvgaCtrl=${_rsc != null} hasMp4Ctrl=${_mp4Controller != null}', ); if (SCGlobalConfig.allowsHighCostAnimations) { if (_dis) { _log( 'manager disposed/no controller; queue play until bind path=$path', ); } final task = SCVapTask( path: path, type: type, priority: resolvedPriority, customResources: customResources, ); SCRoomEffectScheduler().registerHighCostTaskQueued(debugLabel: path); _enqueueTask(task); unawaited(preload(path)); _log('task enqueued path=$path queueAfter=$_queueSummary'); if (!_play) { _pn(); } } else { _log( 'play ignored because performance profile disabled heavy animations ' 'sdkInt=${SCGlobalConfig.sdkInt} maxSdkNoAnim=${SCGlobalConfig.maxSdkNoAnim} ' 'lowPerformance=${SCGlobalConfig.isLowPerformanceDevice}', ); } } // 播放下一个任务 Future _pn() async { if (_pause) { _log('skip _pn because paused queue=$_queueSummary'); return; } if (_dis || _pendingTaskCount == 0 || _play) return; final task = _peekNextQueuedTask(); if (task == null || !_isControllerReady(task)) { _log( 'controller not ready for path=${task?.path} ' 'needSvga=${task != null ? _needsSvgaController(task.path) : "unknown"} ' 'hasSvgaCtrl=${_rsc != null} hasMp4Ctrl=${_mp4Controller != null} ' 'queue=$_queueSummary', ); return; } _removeQueuedTask(task); _play = true; _currentTask = task; _armCurrentTaskWatchdog(task); try { final pathType = SCPathUtils.getPathType(task.path); _log( 'start task path=${task.path} ' 'pathType=$pathType ' 'queueRemaining=$_queueSummary ' 'needSvga=${_needsSvgaController(task.path)} ' 'type=${task.type} consecutiveEntry=$_consecutiveEntryTaskCount', ); if (pathType == PathType.asset) { await _pa(task); } else if (pathType == PathType.file) { await _pf(task); } else if (pathType == PathType.network) { await _pnw(task); } else { _finishCurrentTask(); } } catch (_) { _finishCurrentTask(); } } // 播放资源文件 Future _pa(SCVapTask task) async { if (_dis) return; if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") { _log('play asset svga path=${task.path}'); final entity = await _loadSvgaEntity(task.path); if (!_isCurrentTask(task)) { return; } _log('use prepared asset svga path=${task.path}'); _rsc?.muted = _mute; _rsc?.videoItem = entity; _rsc?.reset(); _rsc?.forward(); _log('forward asset svga path=${task.path}'); } else { _log('play asset vap/mp4 path=${task.path}'); await _mp4Controller?.playAsset(task.path); } } // 播放本地文件 Future _pf(SCVapTask task, {String? playablePath}) async { if (_dis) { return; } final resolvedPath = playablePath ?? task.path; if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") { final entity = await _loadSvgaEntity(resolvedPath); if (!_isCurrentTask(task)) { return; } _log('play local svga file path=$resolvedPath source=${task.path}'); _rsc?.muted = _mute; _rsc?.videoItem = entity; _rsc?.reset(); _rsc?.forward(); return; } if (!_needsMp4Controller(task.path) && !_needsMp4Controller(resolvedPath)) { _log( 'skip playFile because effect file extension is unsupported ' 'path=$resolvedPath source=${task.path}', ); _finishCurrentTask(); return; } if (_mp4Controller == null) { _log('skip playFile because mp4 controller is null path=$resolvedPath'); _finishCurrentTask(); return; } _log('play local vap/mp4 file path=$resolvedPath source=${task.path}'); await _mp4Controller?.setMute(_mute); if (!_isCurrentTask(task)) { return; } await _mp4Controller!.playFile(resolvedPath); } // 播放网络资源 Future _pnw(SCVapTask task) async { if (_dis) return; if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") { _log('play network svga path=${task.path}'); late final MovieEntity entity; try { entity = await _loadSvgaEntity(task.path); } catch (e) { _finishCurrentTask(); return; } if (!_isCurrentTask(task)) { return; } _log('use prepared network svga path=${task.path}'); _rsc?.muted = _mute; _rsc?.videoItem = entity; _rsc?.reset(); _rsc?.forward(); _log('forward network svga path=${task.path}'); } else { _log('download network vap/mp4 path=${task.path}'); final playablePath = await _ensurePlayableFilePath(task.path); if (!_isCurrentTask(task)) { return; } if (!_dis) { _log('use prepared network vap/mp4 local path=$playablePath'); await _pf(task, playablePath: playablePath); } } } // 处理控制器状态变化 void _hcs() { if (_mp4Controller != null && !_dis) { _log('finish mp4 task path=${_currentTask?.path}'); _finishCurrentTask(delay: const Duration(milliseconds: 50)); } } bool _isCurrentTask(SCVapTask task) { return !_dis && _play && identical(_currentTask, task); } void _armCurrentTaskWatchdog(SCVapTask task) { _cancelCurrentTaskWatchdog(); _currentTaskWatchdogTimer = Timer(_taskWatchdogTimeout, () { if (!_isCurrentTask(task)) { return; } _log('task watchdog timeout path=${task.path}'); _mp4Controller?.stop(); _rsc?.stop(); _rsc?.reset(); _rsc?.videoItem = null; _finishCurrentTask(delay: const Duration(milliseconds: 50)); }); } void _cancelCurrentTaskWatchdog() { _currentTaskWatchdogTimer?.cancel(); _currentTaskWatchdogTimer = null; } //暂停动画播放 void pauseAnim() { _pause = true; _log('pauseAnim queue=$_queueSummary'); } //恢复动画播放 void resumeAnim() { _pause = false; _log('resumeAnim queue=$_queueSummary'); _pn(); } void stopPlayback() { _log('stopPlayback queue=$_queueSummary currentPath=${_currentTask?.path}'); _play = false; _currentTask = null; _pause = false; _consecutiveEntryTaskCount = 0; _cancelCurrentTaskWatchdog(); _giftQueue.clear(); _entryQueue.clear(); _preloadQueue.clear(); _queuedPreloadPaths.clear(); _activePreloadCount = 0; _mp4Controller?.stop(); _rsc?.stop(); _rsc?.reset(); _rsc?.videoItem = null; SCRoomEffectScheduler().clearHighCostTasks(reason: 'stop_playback'); } void clearMemoryCache({bool includeCurrent = false}) { _preloadQueue.clear(); _queuedPreloadPaths.clear(); _activePreloadCount = 0; final entries = videoItemCache.entries.toList(growable: false); for (final entry in entries) { final entity = entry.value; if (!includeCurrent && _isSvgaEntityInUse(entity)) { continue; } videoItemCache.remove(entry.key); _disposeSvgaEntity(entity); } _playablePathCache.clear(); } bool _isSvgaEntityInUse(MovieEntity entity) { return identical(_rsc?.videoItem, entity); } void _disposeSvgaEntityIfUnused(MovieEntity entity) { if (_isSvgaEntityInUse(entity)) { return; } _disposeSvgaEntity(entity); } void _disposeSvgaEntity(MovieEntity entity) { try { entity.dispose(); } catch (_) {} } // 释放资源 void dispose() { _log('dispose queue=$_queueSummary currentPath=${_currentTask?.path}'); _dis = true; stopPlayback(); _svgaLoadTasks.clear(); _playablePathTasks.clear(); clearMemoryCache(includeCurrent: true); _playablePathCache.clear(); _mp4Controller?.dispose(); _mp4Controller = null; _rsc?.dispose(); _rsc = null; } // 清除所有任务 void clearTasks() { _log( 'clearTasks queueBefore=$_queueSummary currentPath=${_currentTask?.path}', ); stopPlayback(); } } // 任务模型 // 1. 修改 SCVapTask 类,实现 Comparable class SCVapTask implements Comparable { final String path; final int type; final int priority; final Map? customResources; final DateTime createdAt; final int _seq; static int _nextSeq = 0; SCVapTask({ required this.path, this.priority = 0, this.customResources, this.type = 0, DateTime? createdAt, }) : createdAt = createdAt ?? DateTime.now(), _seq = _nextSeq++; @override int compareTo(SCVapTask other) { // 先按优先级降序排列 int priorityComparison = other.priority.compareTo(priority); if (priorityComparison != 0) { return priorityComparison; } // 相同优先级时,按序列号升序排列(先添加的先执行) return _seq.compareTo(other._seq); } @override String toString() { return 'SCVapTask{path: $path, type: $type, priority: $priority, seq: $_seq}'; } } // 优先队列实现 class SCPriorityQueue { final List _els = []; final Comparator _cmp; SCPriorityQueue(this._cmp); void add(E element) { _els.add(element); _els.sort(_cmp); } E removeFirst() { if (isEmpty) throw StateError("No elements"); return _els.removeAt(0); } E? get first => isEmpty ? null : _els.first; bool get isEmpty => _els.isEmpty; bool get isNotEmpty => _els.isNotEmpty; int get length => _els.length; void clear() => _els.clear(); bool remove(E element) => _els.remove(element); E removeLast() { if (isEmpty) throw StateError("No elements"); return _els.removeLast(); } List get unorderedElements => List.from(_els); // 实现 Iterable 接口 Iterator get iterator => _els.iterator; }