From 2b33c17aab7a534d097de65b1fe6e5ef53767891 Mon Sep 17 00:00:00 2001 From: lxh2875931338 Date: Fri, 23 Oct 2026 21:17:56 +0800 Subject: [PATCH] =?UTF-8?q?fix(playback):=20=E5=A2=9E=E5=8A=A0=E8=BF=9B?= =?UTF-8?q?=E5=BA=A6=E7=9C=8B=E9=97=A8=E7=8B=97=EF=BC=8C=E5=85=9C=E4=BD=8F?= =?UTF-8?q?=E4=B8=8D=E4=BA=A7=E7=94=9F=20buffering=20=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E7=9A=84=E5=8D=8A=E5=BC=80=E8=BF=9E=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 公网链路实测(清远->华为云->香港CN2->openresty->OpenList HTTPS)中 出现过 tcp: ffurl_read 错误但未触发 buffering=true 的情况, 仅靠缓冲事件无法覆盖这类"连接半死"的卡顿。 - 每 10s 检查一次播放进度;播放中且 30s 未推进 → 触发自愈(重开 + seek 回原位) - 切歌中/暂停时重置基准,避免误触发 - 与 buffering 看门狗共用 2 分钟节流 验证:公网路径连续播放 65s,无 false positive,进度正常推进。 --- lib/services/audio_service.dart | 44 +++++++++++++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/lib/services/audio_service.dart b/lib/services/audio_service.dart index c1e1820..9cd2465 100644 --- a/lib/services/audio_service.dart +++ b/lib/services/audio_service.dart @@ -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 _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();