// lib/services/playback_service.dart import 'dart:async'; import 'package:media_kit/media_kit.dart'; import 'package:flutter/foundation.dart'; class PlaybackService { static final PlaybackService _instance = PlaybackService._internal(); factory PlaybackService() => _instance; PlaybackService._internal() { // ⭐ 给单例本身打上 ID _serviceId = DateTime.now().microsecondsSinceEpoch.toString(); debugPrint('🎯 [PlaybackService] 单例创建, serviceId=$_serviceId'); } /// 弱网/源站抖动时,mpv 的 open 可能长时间不返回。 /// 不设上限的话 _playCurrent() 的 finally 永远不执行, /// _isChangingTrack 会永久为 true,整个切歌链路假死。 static const Duration _openTimeout = Duration(seconds: 20); static const Duration _controlTimeout = Duration(seconds: 5); Player? _player; bool _initialized = false; late final String _serviceId; // ⭐ 服务实例 ID // ---------- Getter ---------- Player get player { debugPrint( '🎯 [PlaybackService] get player called, serviceId=$_serviceId, initialized=$_initialized, player=${_player?.hashCode}'); if (!_initialized) init(); return _player!; } bool get isInitialized => _initialized; // ---------- 初始化 ---------- void init() { if (_initialized) { debugPrint( '⚠️ [PlaybackService] init() 被重复调用, 跳过, serviceId=$_serviceId, player=${_player?.hashCode}'); return; } _player = Player(); // 拥塞网络(公司共享出口)下加大缓冲:多存一点余量, // 减少"播到一半断流 -> 重连"的次数。代价是起播稍慢。 // 通过 platform 动态调用,避免依赖 media_kit 的内部类型。 try { final platform = _player!.platform as dynamic; platform.setProperty('cache-secs', '60'); platform.setProperty('demuxer-max-bytes', '64MiB'); platform.setProperty('demuxer-readahead-secs', '30'); debugPrint('✅ [PlaybackService] mpv 缓冲参数已设置(cache 60s / 64MiB)'); } catch (e) { debugPrint('⚠️ [PlaybackService] 设置 mpv 缓冲参数失败: $e'); } _initialized = true; debugPrint( '✅ [PlaybackService] Player 已创建, serviceId=$_serviceId, player=${_player.hashCode}'); } // ---------- 播放 ---------- Future play(String url, {Map? headers}) async { debugPrint( '▶️ [PlaybackService] play() called, serviceId=$_serviceId, player=${_player?.hashCode}, url=${_shortUrl(url)}'); if (!_initialized) init(); final media = (headers != null && headers.isNotEmpty) ? Media(url, httpHeaders: headers) : Media(url); try { final sw = Stopwatch()..start(); await _player!.open(media).timeout(_openTimeout); final openMs = sw.elapsedMilliseconds; await _player!.play().timeout(_controlTimeout); debugPrint('⏱️ [PlaybackService] open 完成 ${openMs}ms,' 'play 完成 ${sw.elapsedMilliseconds}ms'); } on TimeoutException { debugPrint( '❌ [PlaybackService] open timeout (${_openTimeout.inSeconds}s): ${_shortUrl(url)}'); await _safeStop(); rethrow; } catch (e) { debugPrint('❌ [PlaybackService] play failed: $e'); await _safeStop(); rethrow; } } /// 失败时尽量把播放器拉回可用的空闲状态,避免下一次 open 继续卡住。 Future _safeStop() async { try { await _player?.stop().timeout(const Duration(seconds: 3)); } catch (_) { // 忽略:stop 本身失败也不应影响上层解锁 } } /// 日志里不打完整 URL(WebDAV 路径很长,且可能带签名参数) String _shortUrl(String url) { final hash = url.hashCode.toRadixString(16); return url.length <= 48 ? url : '${url.substring(0, 48)}…#$hash'; } // ---------- 控制 ---------- Future pause() async { debugPrint( '⏸️ [PlaybackService] pause() called, serviceId=$_serviceId, player=${_player?.hashCode}'); if (_initialized && _player != null) { await _player!.pause().timeout(_controlTimeout); } } Future resume() async { debugPrint( '▶️ [PlaybackService] resume() called, serviceId=$_serviceId, player=${_player?.hashCode}'); if (_initialized && _player != null) { await _player!.play().timeout(_controlTimeout); } } Future stop() async { debugPrint( '⏹️ [PlaybackService] stop() called, serviceId=$_serviceId, player=${_player?.hashCode}'); if (_initialized && _player != null) { await _player!.stop().timeout(_controlTimeout); } } Future seek(Duration position) async { debugPrint( '⏩ [PlaybackService] seek() called, serviceId=$_serviceId, player=${_player?.hashCode}'); if (_initialized && _player != null) { await _player!.seek(position).timeout(_controlTimeout); } } void setVolume(double volume) { if (_initialized && _player != null) { _player!.setVolume(volume); } } // ---------- 释放 ---------- void dispose() { debugPrint( '🗑️ [PlaybackService] dispose() called, serviceId=$_serviceId, player=${_player?.hashCode}'); if (_initialized && _player != null) { _player!.dispose(); _player = null; _initialized = false; } } // ---------- 重新初始化 ---------- void reinitialize() { debugPrint( '🔄 [PlaybackService] reinitialize() called, serviceId=$_serviceId'); dispose(); init(); } }