From 30e68780f4e7d81dc2a4d827de5b283b543c871e Mon Sep 17 00:00:00 2001 From: lxh2875931338 Date: Sat, 31 Oct 2026 22:03:09 +0800 Subject: [PATCH] =?UTF-8?q?feat(playback):=20=E6=8B=A5=E5=A1=9E=E7=BD=91?= =?UTF-8?q?=E7=BB=9C=E9=80=80=E9=81=BF=E9=87=8D=E8=AF=95=20+=20mpv=20?= =?UTF-8?q?=E7=BC=93=E5=86=B2=E5=8A=A0=E5=A4=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 针对公司共享出口(整栋楼抢网): - 恢复退避:连续失败 3 次后 30s、4 次 60s、5 次 120s, 冷却结束自动再试一次;累计 6 次后停止,等用户手动点 - 用户手动点播放会清零退避计数 - 成功出声(position>1s)后清零 - mpv 缓冲加大:cache-secs=60s、demuxer-max-bytes=64MiB、 demuxer-readahead-secs=30 真机验证:缓冲参数设置成功,起播 1576ms(此前 2098ms)。 --- lib/services/audio_service.dart | 59 +++++++++++++++++++++++++++--- lib/services/playback_service.dart | 14 +++++++ 2 files changed, 67 insertions(+), 6 deletions(-) diff --git a/lib/services/audio_service.dart b/lib/services/audio_service.dart index 9609755..98b40ff 100644 --- a/lib/services/audio_service.dart +++ b/lib/services/audio_service.dart @@ -123,6 +123,17 @@ class AudioService extends ChangeNotifier { DateTime? _lastPlayRequestAt; int _lastPlayRequestIndex = -1; + /// 恢复退避:拥塞网络里连续失败后拉长间隔,避免和整栋楼抢带宽。 + DateTime? _nextRecoveryAt; + Timer? _autoRetryTimer; + + static Duration _backoffFor(int attempts) { + if (attempts <= 2) return Duration.zero; + if (attempts == 3) return const Duration(seconds: 30); + if (attempts == 4) return const Duration(seconds: 60); + return const Duration(seconds: 120); + } + /// 当前歌曲开始加载的时间,用于"起播看门狗": /// 有些情况下 mpv 的 playing 已经是 true,但 position 永远是 0、 /// 也不发 buffering / error(实测公网链路可静默卡 75s+)。 @@ -347,6 +358,8 @@ class AudioService extends ChangeNotifier { // 切歌:位置基准归零,避免上一首的最后位置被当成这一首的"最后已知位置" _lastKnownPosition = Duration.zero; _recoveryAttempts = 0; + _nextRecoveryAt = null; + _autoRetryTimer?.cancel(); try { _playbackGeneration++; @@ -618,6 +631,19 @@ class AudioService extends ChangeNotifier { }); } + /// 冷却结束后的自动重试(只试一次,成功与否交给下一轮判断) + void _scheduleAutoRetry(Duration delay) { + _autoRetryTimer?.cancel(); + _autoRetryTimer = Timer(delay, () { + _nextRecoveryAt = null; + if (_currentSong == null) return; + if (_isChangingTrack) return; + if (positionNotifier.value > const Duration(seconds: 1)) return; + debugPrint('⏰ [AudioService] 冷却结束,再试一次'); + _recoverFromStall(force: true); + }); + } + /// 卡死/网络切换自愈:重开当前歌曲并 seek 回原位置。 /// /// [force] 为 true 时跳过节流(用于网络切换这种明确的外部事件)。 @@ -629,13 +655,26 @@ class AudioService extends ChangeNotifier { final url = song.url; if (url == null || url.isEmpty) return; - // 熔断:同一首歌连续恢复失败,说明不是"卡了一下", - // 而是这个文件/连接根本拿不到数据。继续重试只会刷日志、抢带宽。 + // 熔断 + 退避:拥塞网络里连续失败,说明现在这条链路抢不到带宽。 + // 拉长间隔,冷却结束后自动再试一次;超过上限就停住等用户手动点。 + if (_nextRecoveryAt != null && DateTime.now().isBefore(_nextRecoveryAt!)) { + return; + } + if (_recoveryAttempts >= _maxRecoveryAttempts) { - debugPrint('⛔ [AudioService] 已连续恢复 $_recoveryAttempts 次仍无法播放,' - '停止自动重试:${song.title}'); - _lastPlaybackError = '无法播放:${song.title}'; - notifyListeners(); + final delay = _backoffFor(_recoveryAttempts); + _nextRecoveryAt = DateTime.now().add(delay); + debugPrint('⏳ [AudioService] 已连续恢复 $_recoveryAttempts 次,' + '${delay.inSeconds}s 后再试:${song.title}'); + if (_recoveryAttempts >= _maxRecoveryAttempts + 3) { + // 退避到极限:停住,交给用户手动重试(点播放会清零计数) + debugPrint('⛔ [AudioService] 放弃自动恢复:${song.title}'); + _lastPlaybackError = '网络拥塞,无法播放:${song.title}'; + notifyListeners(); + return; + } + _scheduleAutoRetry(delay); + _recoveryAttempts++; return; } _recoveryAttempts++; @@ -959,6 +998,10 @@ class AudioService extends ChangeNotifier { if (_currentSong == null) return; if (_isPlaying) return; + // 用户手动点播放:重置退避计数,允许立即重试 + _recoveryAttempts = 0; + _nextRecoveryAt = null; + if (_restoredPendingOpen) { _restoredPendingOpen = false; debugPrint('▶️ [AudioService] 冷启动后首次播放,按保存进度打开'); @@ -1083,6 +1126,8 @@ class AudioService extends ChangeNotifier { // 真的出声了(>1s),说明恢复成功,重置熔断计数 if (position > const Duration(seconds: 1)) { _recoveryAttempts = 0; + _nextRecoveryAt = null; + _autoRetryTimer?.cancel(); } // 首次拿到 >0 的播放位置:这是"真正出声"的时刻, @@ -1356,6 +1401,8 @@ class AudioService extends ChangeNotifier { _progressTimer = null; _networkDebounce?.cancel(); _networkDebounce = null; + _autoRetryTimer?.cancel(); + _autoRetryTimer = null; NetworkWatcher().stop(); savePlaybackState(); _stopListening(); diff --git a/lib/services/playback_service.dart b/lib/services/playback_service.dart index ccc2dc3..95cce95 100644 --- a/lib/services/playback_service.dart +++ b/lib/services/playback_service.dart @@ -41,6 +41,20 @@ class PlaybackService { return; } _player = Player(); + + // 拥塞网络(公司共享出口)下加大缓冲:多存一点余量, + // 减少"播到一半断流 -> 重连"的次数。代价是起播稍慢。 + // 通过 platform 动态调用,避免依赖 media_kit 的内部类型。 + try { + final platform = _player!.platform as dynamic; + platform.setProperty('cache-secs', '60'); + platform.setProperty('demuxer-max-bytes', '64MiB'); + platform.setProperty('demuxer-readahead-secs', '30'); + debugPrint('✅ [PlaybackService] mpv 缓冲参数已设置(cache 60s / 64MiB)'); + } catch (e) { + debugPrint('⚠️ [PlaybackService] 设置 mpv 缓冲参数失败: $e'); + } + _initialized = true; debugPrint( '✅ [PlaybackService] Player 已创建, serviceId=$_serviceId, player=${_player.hashCode}');