yumi-flutter/lib/shared/tools/sc_gift_vap_svga_manager.dart
2026-05-09 18:31:48 +08:00

833 lines
23 KiB
Dart

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:tancent_vap/widgets/vap_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<String, MovieEntity> 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<SCVapTask> _giftQueue = SCPriorityQueue<SCVapTask>(
(a, b) => a.compareTo(b), // 调用 SCVapTask 的 compareTo 方法
);
final SCPriorityQueue<SCVapTask> _entryQueue = SCPriorityQueue<SCVapTask>(
(a, b) => a.compareTo(b), // 调用 SCVapTask 的 compareTo 方法
);
VapController? _rgc;
SVGAAnimationController? _rsc;
bool _play = false;
bool _dis = false;
SCVapTask? _currentTask;
Timer? _currentTaskWatchdogTimer;
final Queue<String> _preloadQueue = Queue<String>();
final Set<String> _queuedPreloadPaths = <String>{};
final Map<String, Future<MovieEntity>> _svgaLoadTasks = {};
final Map<String, Future<String>> _playablePathTasks = {};
final Map<String, String> _playablePathCache = {};
int _activePreloadCount = 0;
int _consecutiveEntryTaskCount = 0;
bool _pause = false;
//是否关闭礼物特效声音
bool _mute = false;
void setMute(bool muteMusic) {
_mute = muteMusic;
_rsc?.muted = _mute;
final vapController = _rgc;
if (vapController != null) {
unawaited(vapController.setMute(_mute));
}
unawaited(DataPersistence.setPlayGiftMusic(_mute));
}
bool getMute() {
return _mute;
}
void _log(String message) {}
bool _needsSvgaController(String path) {
return SCPathUtils.getFileExtension(path).toLowerCase() == ".svga";
}
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 _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<void> preload(String path, {bool highPriority = false}) async {
if (!SCGlobalConfig.allowsHighCostAnimations ||
path.isEmpty ||
_dis ||
_isPreloadedOrLoading(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).whenComplete(() {
_activePreloadCount--;
_log(
'finish preload path=$path active=$_activePreloadCount '
'queueRemaining=${_preloadQueue.length}',
);
_drainPreloadQueue();
});
}
Future<void> _warmupPath(String path) async {
if (_needsSvgaController(path)) {
await _loadSvgaEntity(path);
return;
}
await _ensurePlayableFilePath(path);
}
Future<MovieEntity> _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<String> _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) {
videoItemCache.remove(path);
videoItemCache[path] = entity;
_trimResolvedCache(videoItemCache, _maxSvgaCacheEntries);
}
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 _trimResolvedCache<T>(Map<String, T> 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<SCVapTask>();
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<SCVapTask>()
.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 bindVapCtrl(VapController vapController) {
_mute = DataPersistence.getPlayGiftMusic();
_dis = false;
_rgc = vapController;
unawaited(vapController.setMute(_mute));
_log(
'bindVapCtrl hasVapCtrl=${_rgc != null} '
'hasSvgaCtrl=${_rsc != null} queue=$_queueSummary 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) {
_mute = DataPersistence.getPlayGiftMusic();
_dis = false;
_rsc = svgaController;
_rsc?.muted = _mute;
_log(
'bindSvgaCtrl hasSvgaCtrl=${_rsc != null} '
'hasVapCtrl=${_rgc != 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<String, VAPContent>? customResources,
int type = giftEffectType,
}) {
if (path.isEmpty) {
_log('play ignored because path is empty');
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} hasVapCtrl=${_rgc != null}',
);
if (SCGlobalConfig.allowsHighCostAnimations) {
if (_dis) {
_log('play ignored because manager is disposed path=$path');
return;
}
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<void> _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} hasVapCtrl=${_rgc != 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 (e, s) {
_finishCurrentTask();
}
}
// 播放资源文件
Future<void> _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}');
if (task.customResources != null) {
task.customResources?.forEach((k, v) async {
await _rgc?.setVapTagContent(k, v);
});
}
await _rgc?.playAsset(task.path);
}
}
// 播放本地文件
Future<void> _pf(SCVapTask task) async {
if (_dis) {
return;
}
if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") {
final entity = await _loadSvgaEntity(task.path);
if (!_isCurrentTask(task)) {
return;
}
_log('play local svga file path=${task.path}');
_rsc?.muted = _mute;
_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 (!_isCurrentTask(task)) {
return;
}
if (task.customResources != null) {
task.customResources?.forEach((k, v) async {
await _rgc?.setVapTagContent(k, v);
});
}
if (!_isCurrentTask(task)) {
return;
}
await _rgc!.playFile(task.path);
}
// 播放网络资源
Future<void> _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(
SCVapTask(
path: playablePath,
type: task.type,
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));
}
}
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}');
_rgc?.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;
_rgc?.stop();
_rsc?.stop();
_rsc?.reset();
_rsc?.videoItem = null;
SCRoomEffectScheduler().clearHighCostTasks(reason: 'stop_playback');
}
// 释放资源
void dispose() {
_log('dispose queue=$_queueSummary currentPath=${_currentTask?.path}');
_dis = true;
stopPlayback();
_svgaLoadTasks.clear();
_playablePathTasks.clear();
videoItemCache.clear();
_playablePathCache.clear();
_rgc?.dispose();
_rgc = null;
_rsc?.dispose();
_rsc = null;
}
// 清除所有任务
void clearTasks() {
_log(
'clearTasks queueBefore=$_queueSummary currentPath=${_currentTask?.path}',
);
stopPlayback();
}
}
// 任务模型
// 1. 修改 SCVapTask 类,实现 Comparable
class SCVapTask implements Comparable<SCVapTask> {
final String path;
final int type;
final int priority;
final Map<String, VAPContent>? 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<E> {
final List<E> _els = [];
final Comparator<E> _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<E> get unorderedElements => List.from(_els);
// 实现 Iterable 接口
Iterator<E> get iterator => _els.iterator;
}