fix(playback): 修复切歌永久假死,并让元数据抓取不再抢播放带宽
问题(后台播一段时间后无法缓冲,点下一首也没反应,只能重启):
1. _isChangingTrack 是没有超时的锁:_playCurrent() 置 true 后,
只有走完 finally 才会清。mpv 的 open() 在弱网下可能挂很久,
锁就一直不释放,之后 next()/previous() 全被静默拒绝。
2. 元数据用 dio.download 整文件下载,和 mpv 流式播放抢带宽,
20Mbps FRP 下播放流只剩一两百 K/s,缓冲长期为 0。
3. 没有订阅 mpv 的 error/buffering,失败无感知、无法自愈。
改动:
- playback_service: open 20s / 控制命令 5s 超时;失败时 stop 回空闲并上抛异常
- audio_service:
* 去掉「正在切歌就拒绝」的逻辑,改为 latest-wins(epoch 校验,
旧请求自行放弃,只有最新请求解锁)
* 播放失败不再吞掉:记录 lastPlaybackError 并 notifyListeners
* 订阅 stream.error / stream.buffering
* 缓冲超过 30s 自动重连当前歌曲并 seek 回原位置(2 分钟内最多一次)
- file_provider: 元数据改为 Range 只取头部 2MB,流式写入并在切歌时取消上一请求
- home_page: 去掉重复的 PlaybackService().play()(同一首歌会被 open 两次),
登录校验提前
验证:dart analyze 无 error;gradlew assembleDebug BUILD SUCCESSFUL。
真机验证待设备重新连接后进行。
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
// lib/services/playback_service.dart
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
@@ -11,6 +13,12 @@ class PlaybackService {
|
||||
debugPrint('🎯 [PlaybackService] 单例创建, serviceId=$_serviceId');
|
||||
}
|
||||
|
||||
/// 弱网/源站抖动时,mpv 的 open 可能长时间不返回。
|
||||
/// 不设上限的话 _playCurrent() 的 finally 永远不执行,
|
||||
/// _isChangingTrack 会永久为 true,整个切歌链路假死。
|
||||
static const Duration _openTimeout = Duration(seconds: 20);
|
||||
static const Duration _controlTimeout = Duration(seconds: 5);
|
||||
|
||||
Player? _player;
|
||||
bool _initialized = false;
|
||||
late final String _serviceId; // ⭐ 服务实例 ID
|
||||
@@ -41,15 +49,41 @@ class PlaybackService {
|
||||
// ---------- 播放 ----------
|
||||
Future<void> play(String url, {Map<String, String>? headers}) async {
|
||||
debugPrint(
|
||||
'▶️ [PlaybackService] play() called, serviceId=$_serviceId, player=${_player?.hashCode}');
|
||||
'▶️ [PlaybackService] play() called, serviceId=$_serviceId, player=${_player?.hashCode}, url=${_shortUrl(url)}');
|
||||
if (!_initialized) init();
|
||||
|
||||
final media = (headers != null && headers.isNotEmpty)
|
||||
? Media(url, httpHeaders: headers)
|
||||
: Media(url);
|
||||
|
||||
await _player!.open(media);
|
||||
await _player!.play();
|
||||
try {
|
||||
await _player!.open(media).timeout(_openTimeout);
|
||||
await _player!.play().timeout(_controlTimeout);
|
||||
} on TimeoutException {
|
||||
debugPrint(
|
||||
'❌ [PlaybackService] open timeout (${_openTimeout.inSeconds}s): ${_shortUrl(url)}');
|
||||
await _safeStop();
|
||||
rethrow;
|
||||
} catch (e) {
|
||||
debugPrint('❌ [PlaybackService] play failed: $e');
|
||||
await _safeStop();
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 失败时尽量把播放器拉回可用的空闲状态,避免下一次 open 继续卡住。
|
||||
Future<void> _safeStop() async {
|
||||
try {
|
||||
await _player?.stop().timeout(const Duration(seconds: 3));
|
||||
} catch (_) {
|
||||
// 忽略:stop 本身失败也不应影响上层解锁
|
||||
}
|
||||
}
|
||||
|
||||
/// 日志里不打完整 URL(WebDAV 路径很长,且可能带签名参数)
|
||||
String _shortUrl(String url) {
|
||||
final hash = url.hashCode.toRadixString(16);
|
||||
return url.length <= 48 ? url : '${url.substring(0, 48)}…#$hash';
|
||||
}
|
||||
|
||||
// ---------- 控制 ----------
|
||||
@@ -57,7 +91,7 @@ class PlaybackService {
|
||||
debugPrint(
|
||||
'⏸️ [PlaybackService] pause() called, serviceId=$_serviceId, player=${_player?.hashCode}');
|
||||
if (_initialized && _player != null) {
|
||||
await _player!.pause();
|
||||
await _player!.pause().timeout(_controlTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +99,7 @@ class PlaybackService {
|
||||
debugPrint(
|
||||
'▶️ [PlaybackService] resume() called, serviceId=$_serviceId, player=${_player?.hashCode}');
|
||||
if (_initialized && _player != null) {
|
||||
await _player!.play();
|
||||
await _player!.play().timeout(_controlTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +107,7 @@ class PlaybackService {
|
||||
debugPrint(
|
||||
'⏹️ [PlaybackService] stop() called, serviceId=$_serviceId, player=${_player?.hashCode}');
|
||||
if (_initialized && _player != null) {
|
||||
await _player!.stop();
|
||||
await _player!.stop().timeout(_controlTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +115,7 @@ class PlaybackService {
|
||||
debugPrint(
|
||||
'⏩ [PlaybackService] seek() called, serviceId=$_serviceId, player=${_player?.hashCode}');
|
||||
if (_initialized && _player != null) {
|
||||
await _player!.seek(position);
|
||||
await _player!.seek(position).timeout(_controlTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user