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 慢还是数据到得慢。
This commit is contained in:
@@ -87,6 +87,7 @@ class AudioService extends ChangeNotifier {
|
||||
|
||||
/// 卡死判定阈值 / 节流
|
||||
static const Duration _stallThreshold = Duration(seconds: 30);
|
||||
static const Duration _startStallThreshold = Duration(seconds: 15);
|
||||
DateTime? _lastStallRecoveryAt;
|
||||
|
||||
/// 最近一次进入 buffering 的时间(用于卡死判定,不能用计时器重置)
|
||||
@@ -101,6 +102,13 @@ class AudioService extends ChangeNotifier {
|
||||
Duration _lastObservedPosition = Duration.zero;
|
||||
DateTime _lastProgressAt = DateTime.now();
|
||||
|
||||
/// 当前歌曲开始加载的时间,用于"起播看门狗":
|
||||
/// 有些情况下 mpv 的 playing 已经是 true,但 position 永远是 0、
|
||||
/// 也不发 buffering / error(实测公网链路可静默卡 75s+)。
|
||||
DateTime? _trackStartAt;
|
||||
DateTime? _playReadyAt;
|
||||
bool _firstAudioReported = false;
|
||||
|
||||
/// 最近一次播放失败的原因,供 UI 提示;成功播放后自动清空。
|
||||
String? _lastPlaybackError;
|
||||
String? get lastPlaybackError => _lastPlaybackError;
|
||||
@@ -306,7 +314,11 @@ class AudioService extends ChangeNotifier {
|
||||
}
|
||||
|
||||
try {
|
||||
_trackStartAt = DateTime.now();
|
||||
_playReadyAt = null;
|
||||
_firstAudioReported = false;
|
||||
await _playWithHeaders(song.url!);
|
||||
_playReadyAt = DateTime.now();
|
||||
} catch (e) {
|
||||
// 播放失败(超时/401/网络中断):必须走到 finally 解锁,
|
||||
// 并把错误记下来,供上层提示或下一首重试。
|
||||
@@ -428,15 +440,33 @@ class AudioService extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
|
||||
final player = PlaybackService().player;
|
||||
|
||||
// 起播看门狗:playing=true 但 position 一直是 0,且没有任何 buffering/error 事件。
|
||||
// 这种情况 mpv 不会自己恢复,等 30s 的通用规则又太慢,单独用 15s 阈值。
|
||||
final stuckAtStart = pos == Duration.zero &&
|
||||
_trackStartAt != null &&
|
||||
now.difference(_trackStartAt!) >= _startStallThreshold &&
|
||||
player.state.playing;
|
||||
if (stuckAtStart) {
|
||||
debugPrint('🩺 [AudioService] 起播 '
|
||||
'${_startStallThreshold.inSeconds}s 仍停在 0s,触发重连');
|
||||
_trackStartAt = now; // 避免下一次 tick 立刻重复触发
|
||||
_recoverFromStall(force: true);
|
||||
return;
|
||||
}
|
||||
|
||||
final stalledFor = now.difference(_lastProgressAt);
|
||||
final recentlyBuffering = _lastBufferingAt != null &&
|
||||
now.difference(_lastBufferingAt!) < const Duration(seconds: 20);
|
||||
|
||||
if (stalledFor >= _stallThreshold && recentlyBuffering) {
|
||||
// playing==true 也是卡死的充分条件(暂停时 playing 为 false)
|
||||
if (stalledFor >= _stallThreshold &&
|
||||
(player.state.playing || recentlyBuffering)) {
|
||||
debugPrint('🩺 [AudioService] 进度停在 $pos 已 ${stalledFor.inSeconds}s,'
|
||||
'且近期持续缓冲,触发自愈');
|
||||
'触发自愈');
|
||||
_lastProgressAt = now;
|
||||
_recoverFromStall();
|
||||
_recoverFromStall(force: true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,7 +493,11 @@ class AudioService extends ChangeNotifier {
|
||||
final resumeAt = positionNotifier.value;
|
||||
debugPrint('🩺 [AudioService] 尝试重连:${song.title}(从 ${resumeAt.inSeconds}s 继续)');
|
||||
try {
|
||||
_trackStartAt = DateTime.now();
|
||||
_playReadyAt = null;
|
||||
_firstAudioReported = false;
|
||||
await _playWithHeaders(url);
|
||||
_playReadyAt = DateTime.now();
|
||||
if (resumeAt > Duration.zero) {
|
||||
await PlaybackService().seek(resumeAt);
|
||||
positionNotifier.value = resumeAt;
|
||||
@@ -816,6 +850,17 @@ class AudioService extends ChangeNotifier {
|
||||
debugPrint('🎯 [AudioService] positionStream ignored: user seeking');
|
||||
return;
|
||||
}
|
||||
// 首次拿到 >0 的播放位置:这是"真正出声"的时刻,
|
||||
// 与 play() 发起时间相减即可算出起播延迟。
|
||||
if (!_firstAudioReported && position > Duration.zero) {
|
||||
_firstAudioReported = true;
|
||||
final readyAt = _playReadyAt;
|
||||
if (readyAt != null) {
|
||||
final ms = DateTime.now().difference(readyAt).inMilliseconds;
|
||||
debugPrint('🔊 [AudioService] 起播耗时 ${ms}ms(open 完成 -> 出声)');
|
||||
_playReadyAt = null;
|
||||
}
|
||||
}
|
||||
positionNotifier.value = position;
|
||||
}),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user