顺序播放跳曲与首次启动跳曲问题的metadata部分修复完成,测试已通过
This commit is contained in:
@@ -51,7 +51,7 @@ class AudioService extends ChangeNotifier {
|
|||||||
List<int> _shuffledIndices = [];
|
List<int> _shuffledIndices = [];
|
||||||
int _shuffledIndex = -1;
|
int _shuffledIndex = -1;
|
||||||
|
|
||||||
// ---- 当前播放的歌单 ID(用于模式同步) ----
|
// ---- 当前播放的歌单 ID ----
|
||||||
String? _currentPlaylistId;
|
String? _currentPlaylistId;
|
||||||
|
|
||||||
// ---- 高频进度 ----
|
// ---- 高频进度 ----
|
||||||
@@ -68,7 +68,6 @@ class AudioService extends ChangeNotifier {
|
|||||||
bool _hasStartedCurrentPlayback = false;
|
bool _hasStartedCurrentPlayback = false;
|
||||||
bool _isUserSeeking = false;
|
bool _isUserSeeking = false;
|
||||||
|
|
||||||
// ⭐ 播放代数:每次切歌递增,用于校验异步任务是否过期
|
|
||||||
int _playbackGeneration = 0;
|
int _playbackGeneration = 0;
|
||||||
|
|
||||||
void Function(Song)? _onSongChanged;
|
void Function(Song)? _onSongChanged;
|
||||||
@@ -105,7 +104,6 @@ class AudioService extends ChangeNotifier {
|
|||||||
_onSongChanged = callback;
|
_onSongChanged = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 切换播放模式(同步更新歌单) ----
|
|
||||||
void togglePlayMode() {
|
void togglePlayMode() {
|
||||||
switch (_playMode) {
|
switch (_playMode) {
|
||||||
case PlayMode.sequential:
|
case PlayMode.sequential:
|
||||||
@@ -125,7 +123,10 @@ class AudioService extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 设置播放队列 ----
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// 队列管理
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
Future<void> setQueue(List<Song> queue, {int startIndex = 0}) async {
|
Future<void> setQueue(List<Song> queue, {int startIndex = 0}) async {
|
||||||
if (queue.isEmpty) {
|
if (queue.isEmpty) {
|
||||||
_clearQueue();
|
_clearQueue();
|
||||||
@@ -160,7 +161,6 @@ class AudioService extends ChangeNotifier {
|
|||||||
stopPlay();
|
stopPlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 播放指定歌曲 ----
|
|
||||||
Future<void> playSong(Song song) async {
|
Future<void> playSong(Song song) async {
|
||||||
_currentPlaylistId = null;
|
_currentPlaylistId = null;
|
||||||
if (_queue.isEmpty || _queue[_currentIndex].id != song.id) {
|
if (_queue.isEmpty || _queue[_currentIndex].id != song.id) {
|
||||||
@@ -171,7 +171,7 @@ class AudioService extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════
|
||||||
// 播放歌单
|
// 歌单
|
||||||
// ════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
Future<void> playPlaylist(String playlistId, {int startIndex = 0}) async {
|
Future<void> playPlaylist(String playlistId, {int startIndex = 0}) async {
|
||||||
@@ -220,7 +220,7 @@ class AudioService extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════
|
||||||
// ⭐ 播放核心(完整生命周期控制)
|
// ⭐ 播放核心
|
||||||
// ════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
Future<void> _playCurrent() async {
|
Future<void> _playCurrent() async {
|
||||||
@@ -234,7 +234,6 @@ class AudioService extends ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ⭐ 进入切歌状态
|
|
||||||
_isChangingTrack = true;
|
_isChangingTrack = true;
|
||||||
_handlingCompletion = false;
|
_handlingCompletion = false;
|
||||||
_hasStartedCurrentPlayback = false;
|
_hasStartedCurrentPlayback = false;
|
||||||
@@ -253,15 +252,11 @@ class AudioService extends ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ⭐ 关键:等待播放器加载完成
|
|
||||||
await _playWithHeaders(song.url!);
|
await _playWithHeaders(song.url!);
|
||||||
|
|
||||||
// ⭐ 等待播放真正开始
|
|
||||||
await _waitForPlaybackStarted();
|
await _waitForPlaybackStarted();
|
||||||
|
|
||||||
_syncPlayerStateDelayed();
|
_syncPlayerStateDelayed();
|
||||||
|
|
||||||
// ⭐ 只有真正开始播放后才推送通知
|
|
||||||
if (_hasStartedCurrentPlayback) {
|
if (_hasStartedCurrentPlayback) {
|
||||||
_onSongChanged?.call(song);
|
_onSongChanged?.call(song);
|
||||||
_loadMetadataForCurrentSong(generation);
|
_loadMetadataForCurrentSong(generation);
|
||||||
@@ -271,18 +266,15 @@ class AudioService extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ⭐ 等待播放器真正开始播放
|
|
||||||
Future<void> _waitForPlaybackStarted() async {
|
Future<void> _waitForPlaybackStarted() async {
|
||||||
final player = PlaybackService().player;
|
final player = PlaybackService().player;
|
||||||
|
|
||||||
// 如果已经在播放,直接标记
|
|
||||||
if (player.state.playing) {
|
if (player.state.playing) {
|
||||||
_hasStartedCurrentPlayback = true;
|
_hasStartedCurrentPlayback = true;
|
||||||
debugPrint('🎵 [AudioService] playback already started');
|
debugPrint('🎵 [AudioService] playback already started');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 等待 playing 变为 true,超时 3 秒
|
|
||||||
try {
|
try {
|
||||||
await player.stream.playing
|
await player.stream.playing
|
||||||
.where((playing) => playing == true)
|
.where((playing) => playing == true)
|
||||||
@@ -292,18 +284,15 @@ class AudioService extends ChangeNotifier {
|
|||||||
debugPrint('🎵 [AudioService] playback started');
|
debugPrint('🎵 [AudioService] playback started');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('⚠️ [AudioService] wait for playback timeout: $e');
|
debugPrint('⚠️ [AudioService] wait for playback timeout: $e');
|
||||||
// 超时也标记为已开始,避免永久阻塞
|
|
||||||
_hasStartedCurrentPlayback = true;
|
_hasStartedCurrentPlayback = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ⭐ 播放带认证头的 URL
|
|
||||||
Future<void> _playWithHeaders(String url) async {
|
Future<void> _playWithHeaders(String url) async {
|
||||||
final headers = await _getAuthHeadersForUrl(url);
|
final headers = await _getAuthHeadersForUrl(url);
|
||||||
await PlaybackService().play(url, headers: headers);
|
await PlaybackService().play(url, headers: headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ⭐ 获取 URL 对应的认证头(WebDAV 需要,本地文件不需要)
|
|
||||||
Future<Map<String, String>> _getAuthHeadersForUrl(String url) async {
|
Future<Map<String, String>> _getAuthHeadersForUrl(String url) async {
|
||||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||||
try {
|
try {
|
||||||
@@ -324,6 +313,10 @@ class AudioService extends ChangeNotifier {
|
|||||||
return raw.hashCode.toString();
|
return raw.hashCode.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// ⭐ Metadata 加载(四层校验)
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
Future<void> _loadMetadataForCurrentSong(int generation) async {
|
Future<void> _loadMetadataForCurrentSong(int generation) async {
|
||||||
if (_currentIndex < 0 || _currentIndex >= _queue.length) return;
|
if (_currentIndex < 0 || _currentIndex >= _queue.length) return;
|
||||||
|
|
||||||
@@ -343,20 +336,32 @@ class AudioService extends ChangeNotifier {
|
|||||||
fileId: song.id,
|
fileId: song.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 第一层:generation 校验
|
||||||
if (_playbackGeneration != generation) {
|
if (_playbackGeneration != generation) {
|
||||||
debugPrint('⚠️ [AudioService] metadata stale (generation), ignoring');
|
debugPrint('⚠️ [AudioService] metadata stale (generation)');
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (_currentIndex != currentIndex) {
|
|
||||||
debugPrint(
|
|
||||||
'⚠️ [AudioService] metadata stale (index changed), ignoring');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (currentIndex >= _queue.length || _queue[currentIndex].id != songId) {
|
|
||||||
debugPrint('⚠️ [AudioService] metadata stale (song changed), ignoring');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 第二层:索引校验
|
||||||
|
if (_currentIndex != currentIndex) {
|
||||||
|
debugPrint('⚠️ [AudioService] metadata stale (index changed)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第三层:歌曲 ID 校验
|
||||||
|
if (_currentIndex >= _queue.length ||
|
||||||
|
_queue[_currentIndex].id != songId) {
|
||||||
|
debugPrint('⚠️ [AudioService] metadata stale (song changed)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⭐ 第四层:_currentSong 校验(防止状态不同步)
|
||||||
|
if (_currentSong == null || _currentSong!.id != songId) {
|
||||||
|
debugPrint('⚠️ [AudioService] metadata stale: current song mismatch');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.isNotEmpty) {
|
||||||
final updatedSong = Song(
|
final updatedSong = Song(
|
||||||
id: song.id,
|
id: song.id,
|
||||||
title: metadata.title.isNotEmpty ? metadata.title : song.title,
|
title: metadata.title.isNotEmpty ? metadata.title : song.title,
|
||||||
@@ -370,14 +375,14 @@ class AudioService extends ChangeNotifier {
|
|||||||
|
|
||||||
_onSongChanged?.call(updatedSong);
|
_onSongChanged?.call(updatedSong);
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'✅ [AudioService] metadata updated: ${updatedSong.title} - ${updatedSong.artist} (generation $generation)');
|
'✅ [AudioService] metadata updated: ${updatedSong.title} - ${updatedSong.artist} (gen $generation)');
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('⚠️ [AudioService] metadata load failed: $e, using fallback');
|
debugPrint('⚠️ [AudioService] metadata load failed: $e');
|
||||||
_onSongChanged?.call(song);
|
_onSongChanged?.call(song);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 清除当前歌曲的缓存
|
|
||||||
Future<void> clearCurrentSongCache() async {
|
Future<void> clearCurrentSongCache() async {
|
||||||
if (_currentSong == null) return;
|
if (_currentSong == null) return;
|
||||||
final song = _currentSong!;
|
final song = _currentSong!;
|
||||||
@@ -433,10 +438,19 @@ class AudioService extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 下一首 ----
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// ⭐ 上一首 / 下一首(带防并发)
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
Future<void> next() async {
|
Future<void> next() async {
|
||||||
if (_queue.isEmpty) return;
|
if (_queue.isEmpty) return;
|
||||||
|
|
||||||
|
// ⭐ 如果正在切换歌曲,忽略本次请求
|
||||||
|
if (_isChangingTrack) {
|
||||||
|
debugPrint('⚠️ [AudioService] next() ignored: track switch in progress');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (_playMode == PlayMode.shuffle) {
|
if (_playMode == PlayMode.shuffle) {
|
||||||
if (_shuffledIndices.isEmpty) return;
|
if (_shuffledIndices.isEmpty) return;
|
||||||
final nextIdx = (_shuffledIndex + 1) % _shuffledIndices.length;
|
final nextIdx = (_shuffledIndex + 1) % _shuffledIndices.length;
|
||||||
@@ -451,10 +465,15 @@ class AudioService extends ChangeNotifier {
|
|||||||
await _playCurrent();
|
await _playCurrent();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 上一首 ----
|
|
||||||
Future<void> previous() async {
|
Future<void> previous() async {
|
||||||
if (_queue.isEmpty) return;
|
if (_queue.isEmpty) return;
|
||||||
|
|
||||||
|
if (_isChangingTrack) {
|
||||||
|
debugPrint(
|
||||||
|
'⚠️ [AudioService] previous() ignored: track switch in progress');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (_playMode == PlayMode.shuffle) {
|
if (_playMode == PlayMode.shuffle) {
|
||||||
if (_shuffledIndices.isEmpty) return;
|
if (_shuffledIndices.isEmpty) return;
|
||||||
final prevIdx = (_shuffledIndex - 1) % _shuffledIndices.length;
|
final prevIdx = (_shuffledIndex - 1) % _shuffledIndices.length;
|
||||||
@@ -592,26 +611,22 @@ class AudioService extends ChangeNotifier {
|
|||||||
// ⭐ 完整生命周期校验的 completed 监听
|
// ⭐ 完整生命周期校验的 completed 监听
|
||||||
_subscriptions.add(
|
_subscriptions.add(
|
||||||
player.stream.completed.listen((_) {
|
player.stream.completed.listen((_) {
|
||||||
// 第一层:切歌期间屏蔽
|
|
||||||
if (_isChangingTrack) {
|
if (_isChangingTrack) {
|
||||||
debugPrint('⚠️ [AudioService] completed ignored during track switch');
|
debugPrint('⚠️ [AudioService] completed ignored during track switch');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 第二层:从未真正开始播放,忽略
|
|
||||||
if (!_hasStartedCurrentPlayback) {
|
if (!_hasStartedCurrentPlayback) {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'⚠️ [AudioService] completed ignored: playback never started');
|
'⚠️ [AudioService] completed ignored: playback never started');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 第三层:防重入
|
|
||||||
if (_handlingCompletion) {
|
if (_handlingCompletion) {
|
||||||
debugPrint('⚠️ [service] completed ignored: already handling');
|
debugPrint('⚠️ [service] completed ignored: already handling');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 第四层:额外校验 - 当前歌曲必须有有效的 duration
|
|
||||||
final pos = player.state.position;
|
final pos = player.state.position;
|
||||||
final dur = player.state.duration;
|
final dur = player.state.duration;
|
||||||
if (dur.inMilliseconds <= 0) {
|
if (dur.inMilliseconds <= 0) {
|
||||||
@@ -620,10 +635,9 @@ class AudioService extends ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 第五层:位置必须接近歌曲末尾(允许 500ms 误差)
|
|
||||||
if (pos.inMilliseconds < dur.inMilliseconds - 500) {
|
if (pos.inMilliseconds < dur.inMilliseconds - 500) {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'⚠️ [AudioService] completed ignored: position=$pos, duration=$dur, not at end');
|
'⚠️ [AudioService] completed ignored: position=$pos, duration=$dur');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -643,16 +657,13 @@ class AudioService extends ChangeNotifier {
|
|||||||
_subscriptions.clear();
|
_subscriptions.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 播放完成处理 ----
|
|
||||||
void _onPlaybackCompleted() {
|
void _onPlaybackCompleted() {
|
||||||
// 队列状态检查
|
|
||||||
if (_queue.isEmpty || _currentIndex < 0 || _currentIndex >= _queue.length) {
|
if (_queue.isEmpty || _currentIndex < 0 || _currentIndex >= _queue.length) {
|
||||||
debugPrint('⚠️ [service] completed ignored: invalid queue');
|
debugPrint('⚠️ [service] completed ignored: invalid queue');
|
||||||
_handlingCompletion = false;
|
_handlingCompletion = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是最后一首且不是单曲循环模式,不触发 next
|
|
||||||
if (_currentIndex + 1 >= _queue.length && _playMode != PlayMode.repeatOne) {
|
if (_currentIndex + 1 >= _queue.length && _playMode != PlayMode.repeatOne) {
|
||||||
debugPrint('⚠️ [service] completed ignored: last song in queue');
|
debugPrint('⚠️ [service] completed ignored: last song in queue');
|
||||||
_handlingCompletion = false;
|
_handlingCompletion = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user