570 lines
15 KiB
Dart
570 lines
15 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/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;
|
||
}
|