From 98db2321fb6b195808b593f0da380e8e416b2a16 Mon Sep 17 00:00:00 2001 From: lxh2875931338 Date: Sat, 24 Oct 2026 20:04:42 +0800 Subject: [PATCH] =?UTF-8?q?fix(playback):=20=E5=86=B7=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E9=A2=84=E5=8A=A0=E8=BD=BD=E9=9F=B3=E9=A2=91?= =?UTF-8?q?=EF=BC=8C=E9=A6=96=E6=AC=A1=E6=92=AD=E6=94=BE=E6=8C=89=E4=BF=9D?= =?UTF-8?q?=E5=AD=98=E8=BF=9B=E5=BA=A6=E6=89=93=E5=BC=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按今日日志定位:所谓"后台假死"其实是进程被系统回收 (13:14:46 ActivityManager: has died: prcp FGS,当时内存压力下批量杀进程)。 冷启动后旧代码会先联网 play→seek→pause,既慢又会和用户点播放打架。 - restorePlaybackState: 只恢复队列/进度/UI,不联网,不再调用 _loadRestoredPlayback - 新增 playOrResume(): UI 按钮、通知栏、耳机键统一入口; 冷启动首次播放走 _playCurrent(open + seek 到保存位置 + play) - AudioPlayerHandler.play() 改走 playOrResume() - 播放中每 10s 持久化一次进度,冷启动恢复更准确 - 窗口期收紧:网络轮询 10s→3s、切换去抖 3s→2s、 起播看门狗 15s→10s、中段卡死 30s→20s 真机验证(release): 冷启动 8s 内 play 调用 0 次(不再预加载) 按键播放:冷启动首次播放 → open 21ms → restored position 15.751s → 1.5s 出声 --- lib/services/audio_player_handler.dart | 3 +- lib/services/audio_service.dart | 90 ++++++++++++++------------ lib/services/network_watcher.dart | 2 +- 3 files changed, 50 insertions(+), 45 deletions(-) diff --git a/lib/services/audio_player_handler.dart b/lib/services/audio_player_handler.dart index 339da65..dbacb9b 100644 --- a/lib/services/audio_player_handler.dart +++ b/lib/services/audio_player_handler.dart @@ -246,7 +246,8 @@ class AudioPlayerHandler extends audio_service.BaseAudioHandler { @override Future play() async { debugPrint('▶️ [handler] play() called'); - await _playback.resume(); + // 走 AudioService 的统一入口:冷启动首次播放需要先 open + seek + await AudioService().playOrResume(); } @override diff --git a/lib/services/audio_service.dart b/lib/services/audio_service.dart index 032ae5f..a71cb26 100644 --- a/lib/services/audio_service.dart +++ b/lib/services/audio_service.dart @@ -87,8 +87,8 @@ class AudioService extends ChangeNotifier { int _switchEpoch = 0; /// 卡死判定阈值 / 节流 - static const Duration _stallThreshold = Duration(seconds: 30); - static const Duration _startStallThreshold = Duration(seconds: 15); + static const Duration _stallThreshold = Duration(seconds: 20); + static const Duration _startStallThreshold = Duration(seconds: 10); DateTime? _lastStallRecoveryAt; /// 最近一次进入 buffering 的时间(用于卡死判定,不能用计时器重置) @@ -115,6 +115,12 @@ class AudioService extends ChangeNotifier { int _metadataRetryGeneration = -1; bool _firstAudioReported = false; + /// 冷启动恢复后尚未真正打开媒体:首次点击播放走 _playCurrent(含 seek) + bool _restoredPendingOpen = false; + + /// 播放中定时持久化位置(冷启动能回到准确位置) + DateTime _lastPositionSaveAt = DateTime.now(); + /// 最近一次播放失败的原因,供 UI 提示;成功播放后自动清空。 String? _lastPlaybackError; String? get lastPlaybackError => _lastPlaybackError; @@ -299,6 +305,7 @@ class AudioService extends ChangeNotifier { // 「最新请求胜出」:不再因为 _isChangingTrack 为 true 就直接丢弃请求。 // 旧请求会通过 epoch 校验自行放弃后续步骤,避免和新的切歌互相覆盖。 final epoch = ++_switchEpoch; + _restoredPendingOpen = false; _isChangingTrack = true; _handlingCompletion = false; _hasStartedCurrentPlayback = false; @@ -497,13 +504,18 @@ class AudioService extends ChangeNotifier { if (pos != _lastObservedPosition) { _lastObservedPosition = pos; _lastProgressAt = now; + // 每 10s 持久化一次进度:进程被系统回收后,冷启动能回到准确位置 + if (now.difference(_lastPositionSaveAt) >= const Duration(seconds: 10)) { + _lastPositionSaveAt = now; + savePlaybackState(); + } return; } final player = PlaybackService().player; // 起播看门狗:playing=true 但 position 一直是 0,且没有任何 buffering/error 事件。 - // 这种情况 mpv 不会自己恢复,等 30s 的通用规则又太慢,单独用 15s 阈值。 + // 这种情况 mpv 不会自己恢复,通用 20s 规则又太慢,单独用 10s 阈值。 final stuckAtStart = pos == Duration.zero && _trackStartAt != null && now.difference(_trackStartAt!) >= _startStallThreshold && @@ -576,7 +588,7 @@ class AudioService extends ChangeNotifier { /// 这里做 3s 去抖:切换瞬间往往会连续触发多次网络接口变化。 void _onNetworkChanged() { _networkDebounce?.cancel(); - _networkDebounce = Timer(const Duration(seconds: 3), () { + _networkDebounce = Timer(const Duration(seconds: 2), () { if (_currentSong == null) return; if (_isChangingTrack) return; debugPrint('🌐 [AudioService] 网络已切换,主动重连当前歌曲'); @@ -823,10 +835,29 @@ class AudioService extends ChangeNotifier { if (_isPlaying) { PlaybackService().pause(); - } else { - PlaybackService().resume(); + savePlaybackState(); + return; } - // 保存状态(包括进度) + + playOrResume(); + } + + /// 统一的"继续播放"入口:UI 按钮、通知栏、耳机键都走这里。 + /// + /// 冷启动恢复后音频尚未加载,此时 resume() 是在空播放器上调用, + /// 表现就是"点了没反应"。这种情况改走 _playCurrent(open + seek + play)。 + Future playOrResume() async { + if (_currentSong == null) return; + if (_isPlaying) return; + + if (_restoredPendingOpen) { + _restoredPendingOpen = false; + debugPrint('▶️ [AudioService] 冷启动后首次播放,按保存进度打开'); + await _playCurrent(); + return; + } + + await PlaybackService().resume(); savePlaybackState(); } @@ -1159,8 +1190,13 @@ class AudioService extends ChangeNotifier { debugPrint( '♻️ [AudioService] playback state restored: ${song.title} - ${song.artist}'); - // ⭐ 关键:加载音频但不自动播放 - await _loadRestoredPlayback(); + // 冷启动只恢复 UI 状态,不联网预加载。 + // 进程被系统回收后再回来时,预加载(play→seek→pause)会拖慢首屏, + // 还会和用户点播放的操作打架,表现为"假死十几秒"。 + _restoredPendingOpen = true; + _isPlaying = false; + positionNotifier.value = _pendingSeekPosition ?? Duration.zero; + notifyListeners(); return true; } catch (e) { @@ -1169,40 +1205,8 @@ class AudioService extends ChangeNotifier { } } - /// ⭐ 加载恢复的播放状态(加载音频,不自动播放) - Future _loadRestoredPlayback() async { - if (_queue.isEmpty || _currentIndex < 0) return; - final song = _queue[_currentIndex]; - if (song.url == null || song.url!.isEmpty) return; - - // 启动监听 - _startListening(); - - // 加载音频(带 headers) - final headers = await _getAuthHeadersForUrl(song.url!); - await PlaybackService().play(song.url!, headers: headers); - - // 等待播放器准备好 - await _waitForPlaybackStarted(); - - // 如果有待恢复的进度,执行 seek - if (_pendingSeekPosition != null && - _pendingSeekPosition!.inMilliseconds > 0) { - final pos = _pendingSeekPosition!; - _pendingSeekPosition = null; - await PlaybackService().seek(pos); - positionNotifier.value = pos; - debugPrint('🎯 [AudioService] restored position: $pos'); - } - - // ⭐ 加载完成后立即暂停(用户点击播放按钮后才继续) - await PlaybackService().pause(); - _isPlaying = false; - _hasStartedCurrentPlayback = true; - notifyListeners(); - - debugPrint('🎵 [AudioService] restored playback loaded and paused'); - } + // 说明:冷启动不再预加载音频(原 _loadRestoredPlayback)。 + // 恢复只做 UI 状态,首次播放时由 togglePlay → _playCurrent 完成 open + seek。 @override void dispose() { diff --git a/lib/services/network_watcher.dart b/lib/services/network_watcher.dart index cb8c3b1..126f4ef 100644 --- a/lib/services/network_watcher.dart +++ b/lib/services/network_watcher.dart @@ -25,7 +25,7 @@ class NetworkWatcher { /// 网络签名变化时回调(已在内部做过去抖) void Function()? onChanged; - void start({Duration interval = const Duration(seconds: 10)}) { + void start({Duration interval = const Duration(seconds: 3)}) { if (_timer != null) return; _timer = Timer.periodic(interval, (_) => _tick()); _tick();