feat(playback): 下一首预热(60% 时 HEAD 预握手)

日志实测:open 完成仅 37~60ms,但起播耗时 2.1~2.8s,
说明等待发生在 mpv 拿首批数据,而不是建连。

- 新增 PrefetchService:HEAD + Range 0-0,只走 DNS/TLS/反代,
  不下载正文;失败静默,不影响播放
- AudioService 在进度达到 60%(且时长 > 60s)时预热下一首,
  每首歌只触发一次;shuffle 模式下按当前随机索引推算

待验证:播到 60% 时应出现
  🔥 [AudioService] 预热下一首:xxx
  🔥 [Prefetch] 预热完成 xxxms,status=200/206
对比下一首的 "起播耗时" 是否下降。
This commit is contained in:
2026-10-23 23:06:18 +08:00
parent 111369c20c
commit 2d571eaae4
2 changed files with 104 additions and 0 deletions
+36
View File
@@ -8,6 +8,7 @@ import 'package:media_kit/media_kit.dart' as media_kit;
import 'package:path_provider/path_provider.dart';
import 'playback_service.dart';
import 'network_watcher.dart';
import 'prefetch_service.dart';
import '../metadata/metadata_service.dart';
import '../database/song_database.dart';
import '../utils/artwork_helper.dart';
@@ -107,6 +108,7 @@ class AudioService extends ChangeNotifier {
/// 也不发 buffering / error(实测公网链路可静默卡 75s+)。
DateTime? _trackStartAt;
DateTime? _playReadyAt;
int _prefetchedGeneration = -1;
bool _firstAudioReported = false;
/// 最近一次播放失败的原因,供 UI 提示;成功播放后自动清空。
@@ -418,6 +420,39 @@ class AudioService extends ChangeNotifier {
}();
}
/// 播到 60% 时预热下一首(HEAD + Range 0-0,不下载正文)。
///
/// 日志实测起播延迟 2~3s 都在"等首批数据",提前完成 DNS/TLS/反代握手
/// 可以把这段时间压掉一部分;失败完全静默,不影响当前播放。
void _maybePrefetchNext(Duration position) {
final duration = durationNotifier.value;
if (duration.inSeconds < 60) return;
if (position.inMilliseconds < duration.inMilliseconds * 0.6) return;
if (_prefetchedGeneration == _playbackGeneration) return;
final next = _peekNextSong();
final url = next?.url;
if (url == null || url.isEmpty) return;
_prefetchedGeneration = _playbackGeneration;
debugPrint('🔥 [AudioService] 预热下一首:${next!.title}');
PrefetchService().warm(url);
}
/// 只推算下一首,不改变队列/索引(next() 会改)。
Song? _peekNextSong() {
if (_queue.isEmpty) return null;
if (_playMode == PlayMode.shuffle) {
if (_shuffledIndices.isEmpty) return null;
final nextIdx = (_shuffledIndex + 1) % _shuffledIndices.length;
return _queue[_shuffledIndices[nextIdx]];
}
final nextIdx = (_currentIndex + 1) % _queue.length;
return _queue[nextIdx];
}
/// 进度看门狗:播放中进度 30s 不动、且近期出现过缓冲,判定为卡死。
///
/// 为什么不能只看 `buffering`:弱网下它会在 true/false 之间反复抖动,
@@ -861,6 +896,7 @@ class AudioService extends ChangeNotifier {
_playReadyAt = null;
}
}
_maybePrefetchNext(position);
positionNotifier.value = position;
}),
);