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();