fix(playback): 打开失败后 1.5s 立即重试,切歌前摇从 ~20s 降到 ~7s

实测时间线(20:08:53 那次切歌):
  53.881 next -> play
  53.947 open 完成 65ms
  59.525 player error: tcp ffurl_read / Failed to open   (mpv 5.6s 后放弃)
  09:13.801 起播看门狗才触发重连                        (又等 14s)

原因:看门狗 5s 一跳、阈值 10s,最坏要两个 tick 才触发。

- error 流里识别 Failed to open / ffurl_read,1.5s 后立即重试一次
  (每首歌最多一次,force 模式跳过节流)
- 进度看门狗 tick 10s -> 5s,任何卡死最多多等 5s

预期:失败发生在 5~6s,重试在 7~8s 开始,前摇 ~10s 降到 7~8s;
若首次打开成功则完全无感。
This commit is contained in:
2026-10-24 20:14:31 +08:00
parent 98db2321fb
commit 9f53d1c604
+29 -1
View File
@@ -121,6 +121,9 @@ class AudioService extends ChangeNotifier {
/// 播放中定时持久化位置(冷启动能回到准确位置)
DateTime _lastPositionSaveAt = DateTime.now();
/// 打开失败后的快速重试(每首歌最多一次)
int _openRetryGeneration = -1;
/// 最近一次播放失败的原因,供 UI 提示;成功播放后自动清空。
String? _lastPlaybackError;
String? get lastPlaybackError => _lastPlaybackError;
@@ -542,6 +545,25 @@ class AudioService extends ChangeNotifier {
}
}
/// 打开失败(ffurl_read / Failed to open)后 1.5s 立即重试一次。
///
/// 实测:切歌后 mpv 在 5~6s 时放弃打开,旧逻辑要等起播看门狗
/// (5s 一跳 × 10s 阈值,最坏 20s)才重连,表现为十几秒的前摇。
/// 每首歌最多快速重试一次,避免失败风暴。
void _scheduleOpenRetry() {
if (_currentSong == null) return;
if (_isChangingTrack) return;
if (_openRetryGeneration == _playbackGeneration) return;
_openRetryGeneration = _playbackGeneration;
debugPrint('🔁 [AudioService] 打开失败,1.5s 后自动重试');
Timer(const Duration(milliseconds: 1500), () {
if (_currentSong == null) return;
if (_isChangingTrack) return;
_recoverFromStall(force: true);
});
}
/// 卡死/网络切换自愈:重开当前歌曲并 seek 回原位置。
///
/// [force] 为 true 时跳过节流(用于网络切换这种明确的外部事件)。
@@ -988,6 +1010,12 @@ class AudioService extends ChangeNotifier {
debugPrint('❌ [AudioService] player error: $error');
_lastPlaybackError = error;
notifyListeners();
// 打开失败(ffurl_read / Failed to open)时立刻安排一次重试,
// 不要等 10~20s 的起播看门狗。
if (error.contains('Failed to open') ||
error.contains('ffurl_read')) {
_scheduleOpenRetry();
}
}),
);
@@ -1009,7 +1037,7 @@ class AudioService extends ChangeNotifier {
_progressTimer?.cancel();
_lastProgressAt = DateTime.now();
_progressTimer = Timer.periodic(
const Duration(seconds: 10),
const Duration(seconds: 5),
(_) => _checkPlaybackProgress(),
);