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:
@@ -89,6 +89,12 @@ class AudioService extends ChangeNotifier {
|
|||||||
Timer? _stallTimer;
|
Timer? _stallTimer;
|
||||||
DateTime? _lastStallRecoveryAt;
|
DateTime? _lastStallRecoveryAt;
|
||||||
|
|
||||||
|
/// 进度看门狗:某些卡死不会触发 buffering 事件(例如 CDN 半开连接),
|
||||||
|
/// 用「播放中进度长时间不动」兜底。
|
||||||
|
Timer? _progressTimer;
|
||||||
|
Duration _lastObservedPosition = Duration.zero;
|
||||||
|
DateTime _lastProgressAt = DateTime.now();
|
||||||
|
|
||||||
/// 最近一次播放失败的原因,供 UI 提示;成功播放后自动清空。
|
/// 最近一次播放失败的原因,供 UI 提示;成功播放后自动清空。
|
||||||
String? _lastPlaybackError;
|
String? _lastPlaybackError;
|
||||||
String? get lastPlaybackError => _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]。
|
/// 触发条件:mpv 的 buffering 持续为 true 超过 [_stallThreshold]。
|
||||||
/// 两次自动恢复之间至少间隔 2 分钟,避免源站长时间不可用时反复重连。
|
/// 两次自动恢复之间至少间隔 2 分钟,避免源站长时间不可用时反复重连。
|
||||||
Future<void> _recoverFromStall() async {
|
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 监听
|
// ⭐ 完整生命周期校验的 completed 监听
|
||||||
_subscriptions.add(
|
_subscriptions.add(
|
||||||
player.stream.completed.listen((_) {
|
player.stream.completed.listen((_) {
|
||||||
@@ -969,6 +1007,8 @@ class AudioService extends ChangeNotifier {
|
|||||||
// 先保存再清理
|
// 先保存再清理
|
||||||
_stallTimer?.cancel();
|
_stallTimer?.cancel();
|
||||||
_stallTimer = null;
|
_stallTimer = null;
|
||||||
|
_progressTimer?.cancel();
|
||||||
|
_progressTimer = null;
|
||||||
savePlaybackState();
|
savePlaybackState();
|
||||||
_stopListening();
|
_stopListening();
|
||||||
positionNotifier.dispose();
|
positionNotifier.dispose();
|
||||||
|
|||||||
Reference in New Issue
Block a user