chatapp3-flutter/lib/shared/tools/sc_gift_vap_svga_manager.dart
2026-04-15 23:01:47 +08:00

570 lines
15 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<String, MovieEntity> videoItemCache = {};
static SCGiftVapSvgaManager? _inst;
static const int _maxPreloadConcurrency = 1;
SCGiftVapSvgaManager._internal();
factory SCGiftVapSvgaManager() => _inst ??= SCGiftVapSvgaManager._internal();
final SCPriorityQueue<SCVapTask> _tq = SCPriorityQueue<SCVapTask>(
(a, b) => a.compareTo(b), // 调用 SCVapTask 的 compareTo 方法
);
VapController? _rgc;
SVGAAnimationController? _rsc;
bool _play = false;
bool _dis = false;
SCVapTask? _currentTask;
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;
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<void> 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<void> _warmupPath(String path) async {
if (_needsSvgaController(path)) {
await _loadSvgaEntity(path);
return;
}
await _ensurePlayableFilePath(path);
}
Future<MovieEntity> _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<String> _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<String, VAPContent>? 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<void> _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<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);
_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<void> _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<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) {
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 stopPlayback() {
_log('stopPlayback queue=${_tq.length} currentPath=${_currentTask?.path}');
_play = false;
_currentTask = null;
_pause = false;
_tq.clear();
_preloadQueue.clear();
_queuedPreloadPaths.clear();
_activePreloadCount = 0;
_rgc?.stop();
_rsc?.stop();
_rsc?.reset();
_rsc?.videoItem = null;
}
// 释放资源
void dispose() {
_log('dispose queue=${_tq.length} currentPath=${_currentTask?.path}');
_dis = true;
stopPlayback();
_rgc?.dispose();
_rgc = null;
_rsc?.dispose();
_rsc = null;
}
// 清除所有任务
void clearTasks() {
_log(
'clearTasks queueBefore=${_tq.length} 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 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<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();
List<E> get unorderedElements => List.from(_els);
// 实现 Iterable 接口
Iterator<E> get iterator => _els.iterator;
}