Files
QingTing-Player/lib/services/playback_service.dart
T
lxh2875931338 111369c20c feat(playback): 起播看门狗 + 起播耗时打点
现象(Plastic Love):state=PLAYING、position 恒为 0,75 秒内没有任何
buffering/error 日志,mpv 既不报错也不推进。通用看门狗要求"近期有缓冲",
因此完全没反应。

- 新增起播看门狗:position 持续为 0 超过 15s 且 playing=true → 强制重连
  (不再要求 buffering 事件;暂停时 playing=false,不会误触发)
- 中段卡死判定也放宽为 playing || recentlyBuffering
- 打点:
  * PlaybackService: "open 完成 xxxms,play 完成 xxxms"
  * AudioService: "起播耗时 xxxms(open 完成 -> 出声)"
  下次再出现"下一首很久",日志能直接区分是 open 慢还是数据到得慢。
2026-10-23 21:55:13 +08:00

151 lines
4.8 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.
// 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();
_initialized = true;
debugPrint(
'✅ [PlaybackService] Player 已创建, serviceId=$_serviceId, player=${_player.hashCode}');
}
// ---------- 播放 ----------
Future<void> play(String url, {Map<String, String>? 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<void> _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<void> pause() async {
debugPrint(
'⏸️ [PlaybackService] pause() called, serviceId=$_serviceId, player=${_player?.hashCode}');
if (_initialized && _player != null) {
await _player!.pause().timeout(_controlTimeout);
}
}
Future<void> resume() async {
debugPrint(
'▶️ [PlaybackService] resume() called, serviceId=$_serviceId, player=${_player?.hashCode}');
if (_initialized && _player != null) {
await _player!.play().timeout(_controlTimeout);
}
}
Future<void> stop() async {
debugPrint(
'⏹️ [PlaybackService] stop() called, serviceId=$_serviceId, player=${_player?.hashCode}');
if (_initialized && _player != null) {
await _player!.stop().timeout(_controlTimeout);
}
}
Future<void> 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();
}
}