fix(playback): 增加进度看门狗,兜住不产生 buffering 事件的半开连接

公网链路实测(清远->华为云->香港CN2->openresty->OpenList HTTPS)中
出现过 tcp: ffurl_read 错误但未触发 buffering=true 的情况,
仅靠缓冲事件无法覆盖这类"连接半死"的卡顿。

- 每 10s 检查一次播放进度;播放中且 30s 未推进 → 触发自愈(重开 + seek 回原位)
- 切歌中/暂停时重置基准,避免误触发
- 与 buffering 看门狗共用 2 分钟节流

验证:公网路径连续播放 65s,无 false positive,进度正常推进。
This commit is contained in:
2026-10-23 21:18:02 +08:00
parent 9762569832
commit 2b33c17aab
+42 -2
View File
@@ -89,6 +89,12 @@ class AudioService extends ChangeNotifier {
Timer? _stallTimer;
DateTime? _lastStallRecoveryAt;
/// 进度看门狗:某些卡死不会触发 buffering 事件(例如 CDN 半开连接),
/// 用「播放中进度长时间不动」兜底。
Timer? _progressTimer;
Duration _lastObservedPosition = Duration.zero;
DateTime _lastProgressAt = DateTime.now();
/// 最近一次播放失败的原因,供 UI 提示;成功播放后自动清空。
String? _lastPlaybackError;
String? get lastPlaybackError => _lastPlaybackError;
@@ -344,8 +350,31 @@ class AudioService extends ChangeNotifier {
}
}
/// 缓冲卡死自愈:重开当前歌曲并 seek 回原位置。
///
/// 进度看门狗:播放中但进度 30 秒没动,说明流已经卡死(半开连接/CDN 卡住)。
void _checkPlaybackProgress() {
if (_isChangingTrack || !_isPlaying) {
// 切歌中或暂停时不参与判断,重置基准
_lastObservedPosition = positionNotifier.value;
_lastProgressAt = DateTime.now();
return;
}
final pos = positionNotifier.value;
if (pos != _lastObservedPosition) {
_lastObservedPosition = pos;
_lastProgressAt = DateTime.now();
return;
}
if (DateTime.now().difference(_lastProgressAt) >= _stallThreshold) {
debugPrint('🩺 [AudioService] 播放进度卡在 $pos 超过 '
'${_stallThreshold.inSeconds}s,触发自愈');
_lastProgressAt = DateTime.now();
_recoverFromStall();
}
}
/// 缓冲卡死自愈:重开当前歌曲并 seek 回原位置。 ///
/// 触发条件:mpv 的 buffering 持续为 true 超过 [_stallThreshold]。
/// 两次自动恢复之间至少间隔 2 分钟,避免源站长时间不可用时反复重连。
Future<void> _recoverFromStall() async {
@@ -746,6 +775,15 @@ class AudioService extends ChangeNotifier {
}),
);
// 进度看门狗:有些半开连接不会产生 buffering=true,
// 但播放进度会一直停在原地,这里兜住这种情况。
_progressTimer?.cancel();
_lastProgressAt = DateTime.now();
_progressTimer = Timer.periodic(
const Duration(seconds: 10),
(_) => _checkPlaybackProgress(),
);
// ⭐ 完整生命周期校验的 completed 监听
_subscriptions.add(
player.stream.completed.listen((_) {
@@ -969,6 +1007,8 @@ class AudioService extends ChangeNotifier {
// 先保存再清理
_stallTimer?.cancel();
_stallTimer = null;
_progressTimer?.cancel();
_progressTimer = null;
savePlaybackState();
_stopListening();
positionNotifier.dispose();