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/data_sources/sources/local/file_cache_manager.dart'; import 'package:tancent_vap/utils/constant.dart'; import 'package:tancent_vap/widgets/vap_view.dart'; import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; class SCGiftVapSvgaManager { Map videoItemCache = {}; static SCGiftVapSvgaManager? _inst; static const int _maxPreloadConcurrency = 1; SCGiftVapSvgaManager._internal(); factory SCGiftVapSvgaManager() => _inst ??= SCGiftVapSvgaManager._internal(); final SCPriorityQueue _tq = SCPriorityQueue( (a, b) => a.compareTo(b), // 调用 SCVapTask 的 compareTo 方法 ); VapController? _rgc; SVGAAnimationController? _rsc; bool _play = false; bool _dis = false; SCVapTask? _currentTask; final Queue _preloadQueue = Queue(); final Set _queuedPreloadPaths = {}; final Map> _svgaLoadTasks = {}; final Map> _playablePathTasks = {}; final Map _playablePathCache = {}; int _activePreloadCount = 0; bool _pause = false; //是否关闭礼物特效声音 bool _mute = false; void setMute(bool muteMusic) { _mute = muteMusic; DataPersistence.setPlayGiftMusic(_mute); } bool getMute() { return _mute; } void _log(String message) { debugPrint('[GiftFX][Player] $message'); } bool _needsSvgaController(String path) { return SCPathUtils.getFileExtension(path).toLowerCase() == ".svga"; } bool _isControllerReady(SCVapTask task) { if (_needsSvgaController(task.path)) { return _rsc != null; } return _rgc != 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 (path.isEmpty || _dis || _isPreloadedOrLoading(path)) { return; } if (highPriority) { _log('high priority preload path=$path'); await _warmupPath(path); return; } if (_queuedPreloadPaths.contains(path)) { return; } _preloadQueue.add(path); _queuedPreloadPaths.add(path); _log('enqueue preload path=$path queue=${_preloadQueue.length}'); _drainPreloadQueue(); } 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).whenComplete(() { _activePreloadCount--; _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 = videoItemCache[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; videoItemCache[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 = _playablePathCache[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); _playablePathCache[path] = file.path; return file.path; }(); _playablePathTasks[path] = future; try { return await future; } finally { _playablePathTasks.remove(path); } } 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; } _play = false; _currentTask = null; _scheduleNextTask(delay: delay); if (delay == Duration.zero) { Future.microtask(_drainPreloadQueue); } else { Future.delayed(delay, _drainPreloadQueue); } } // 绑定控制器 void bindVapCtrl(VapController vapController) { _mute = DataPersistence.getPlayGiftMusic(); _dis = false; _rgc = vapController; _log( 'bindVapCtrl hasVapCtrl=${_rgc != null} ' 'hasSvgaCtrl=${_rsc != null} queue=${_tq.length} mute=$_mute', ); _rgc?.setAnimListener( onVideoStart: () { _log('vap onVideoStart path=${_currentTask?.path}'); }, onVideoComplete: () { _log('vap onVideoComplete path=${_currentTask?.path}'); _hcs(); }, onFailed: (code, type, msg) { _log( 'vap onFailed path=${_currentTask?.path} code=$code type=$type msg=$msg', ); _hcs(); }, ); _scheduleNextTask(); _drainPreloadQueue(); } void bindSvgaCtrl(SVGAAnimationController svgaController) { _dis = false; _rsc = svgaController; _log( 'bindSvgaCtrl hasSvgaCtrl=${_rsc != null} ' 'hasVapCtrl=${_rgc != null} queue=${_tq.length}', ); _rsc?.addStatusListener((AnimationStatus status) { if (status.isCompleted) { _log('svga completed path=${_currentTask?.path}'); _rsc?.reset(); _play = false; _currentTask = null; _pn(); } }); _scheduleNextTask(); _drainPreloadQueue(); } // 播放任务 void play( String path, { int priority = 0, Map? customResources, int type = 0, }) { if (path.isEmpty) { _log('play ignored because path is empty'); return; } _log( 'play request path=$path ext=${SCPathUtils.getFileExtension(path)} ' 'priority=$priority type=$type ' 'sdkInt=${SCGlobalConfig.sdkInt} maxSdkNoAnim=${SCGlobalConfig.maxSdkNoAnim} ' 'disposed=$_dis playing=$_play queueBefore=${_tq.length} ' 'hasSvgaCtrl=${_rsc != null} hasVapCtrl=${_rgc != null}', ); if (SCGlobalConfig.sdkInt > SCGlobalConfig.maxSdkNoAnim) { if (_dis) { _log('play ignored because manager is disposed path=$path'); return; } final task = SCVapTask( path: path, priority: priority, customResources: customResources, ); _tq.add(task); _log('task enqueued path=$path queueAfter=${_tq.length}'); if (!_play) { _pn(); } } else { _log( 'play ignored because sdkInt <= maxSdkNoAnim ' 'sdkInt=${SCGlobalConfig.sdkInt} maxSdkNoAnim=${SCGlobalConfig.maxSdkNoAnim}', ); } } // 播放下一个任务 Future _pn() async { if (_pause) { _log('skip _pn because paused queue=${_tq.length}'); return; } if (_dis || _tq.isEmpty || _play) return; final task = _tq.first; if (task == null || !_isControllerReady(task)) { _log( 'controller not ready for path=${task?.path} ' 'needSvga=${task != null ? _needsSvgaController(task.path) : "unknown"} ' 'hasSvgaCtrl=${_rsc != null} hasVapCtrl=${_rgc != null} ' 'queue=${_tq.length}', ); return; } _tq.removeFirst(); _play = true; _currentTask = task; try { final pathType = SCPathUtils.getPathType(task.path); _log( 'start task path=${task.path} ' 'pathType=$pathType ' 'queueRemaining=${_tq.length} ' 'needSvga=${_needsSvgaController(task.path)}', ); if (pathType == PathType.asset) { await _pa(task); } else if (pathType == PathType.file) { await _pf(task); } else if (pathType == PathType.network) { await _pnw(task); } else { debugPrint('VAP_SVGA不支持的路径类型: ${task.path}'); _finishCurrentTask(); } } catch (e, s) { debugPrint('VAP_SVGA播放失败: $e\n$s'); _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); _log('use prepared asset svga path=${task.path}'); _rsc?.videoItem = entity; _rsc?.reset(); _rsc?.forward(); _log('forward asset svga path=${task.path}'); } else { _log('play asset vap/mp4 path=${task.path}'); if (task.customResources != null) { task.customResources?.forEach((k, v) async { await _rgc?.setVapTagContent(k, v); }); } await _rgc?.playAsset(task.path); } } // 播放本地文件 Future _pf(SCVapTask task) async { if (_dis) { return; } if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") { final entity = await _loadSvgaEntity(task.path); _log('play local svga file path=${task.path}'); _rsc?.videoItem = entity; _rsc?.reset(); _rsc?.forward(); return; } if (_rgc == null) { _log('skip playFile because vap controller is null path=${task.path}'); _finishCurrentTask(); return; } _log('play local vap/mp4 file path=${task.path}'); await _rgc?.setMute(_mute); if (task.customResources != null) { task.customResources?.forEach((k, v) async { await _rgc?.setVapTagContent(k, v); }); } await _rgc!.playFile(task.path); } // 播放网络资源 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) { debugPrint('svga解析出错:$e'); _finishCurrentTask(); return; } _log('use prepared network svga path=${task.path}'); _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 (!_dis) { _log('use prepared network vap/mp4 local path=$playablePath'); await _pf( SCVapTask( path: playablePath, priority: task.priority, customResources: task.customResources, ), ); } } } // 处理控制器状态变化 void _hcs() { if (_rgc != null && !_dis) { _log('finish vap task path=${_currentTask?.path}'); _finishCurrentTask(delay: const Duration(milliseconds: 50)); } } //暂停动画播放 void pauseAnim() { _pause = true; _log('pauseAnim queue=${_tq.length}'); } //恢复动画播放 void resumeAnim() { _pause = false; _log('resumeAnim queue=${_tq.length}'); _pn(); } // 释放资源 void dispose() { _log('dispose queue=${_tq.length} currentPath=${_currentTask?.path}'); _dis = true; _play = false; _currentTask = null; _tq.clear(); _preloadQueue.clear(); _queuedPreloadPaths.clear(); _activePreloadCount = 0; _rgc?.stop(); _rgc?.dispose(); _rgc = null; _rsc?.stop(); _rsc?.dispose(); _rsc = null; } // 清除所有任务 void clearTasks() { _log( 'clearTasks queueBefore=${_tq.length} currentPath=${_currentTask?.path}', ); _play = false; _currentTask = null; _tq.clear(); _preloadQueue.clear(); _queuedPreloadPaths.clear(); _activePreloadCount = 0; _pause = false; } } // 任务模型 // 1. 修改 SCVapTask 类,实现 Comparable class SCVapTask implements Comparable { final String path; final int type; final int priority; final Map? customResources; final int _seq; static int _nextSeq = 0; SCVapTask({ required this.path, this.priority = 0, this.customResources, this.type = 0, }) : _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, 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(); List get unorderedElements => List.from(_els); // 实现 Iterable 接口 Iterator get iterator => _els.iterator; }