Files
QingTing-Player/lib/services/audio_service.dart
T
lxh2875931338 9e58d52678 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。
真机验证待设备重新连接后进行。
2026-10-23 21:04:45 +08:00

981 lines
30 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// lib/services/audio_service.dart
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:media_kit/media_kit.dart' as media_kit;
import 'package:path_provider/path_provider.dart';
import 'playback_service.dart';
import '../metadata/metadata_service.dart';
import '../database/song_database.dart';
import '../utils/artwork_helper.dart';
import '../utils/song_key.dart';
import '../repositories/playlist_repository.dart';
import '../models/playlist.dart';
import 'webdav_service.dart';
import 'audio_player_handler.dart';
enum PlayMode {
sequential,
repeatOne,
shuffle,
}
class Song {
final String id;
final String title;
final String artist;
final String? url;
final Uint8List? artwork;
Song({
required this.id,
required this.title,
required this.artist,
this.url,
this.artwork,
});
Map<String, dynamic> toJson() => {
'id': id,
'title': title,
'artist': artist,
'url': url,
};
}
class AudioService extends ChangeNotifier {
static final AudioService _instance = AudioService._internal();
factory AudioService() => _instance;
AudioService._internal();
// ---- 基础状态 ----
Song? _currentSong;
bool _isPlaying = false;
PlayMode _playMode = PlayMode.sequential;
// ---- 播放队列 ----
List<Song> _queue = [];
int _currentIndex = -1;
List<int> _shuffledIndices = [];
int _shuffledIndex = -1;
// ---- 当前播放的歌单 ID ----
String? _currentPlaylistId;
// ⭐ 插入位置:在现有成员变量之后,方法之前
AudioPlayerHandler? _handler; // ⭐ 添加这一行
// ---- 高频进度 ----
final ValueNotifier<Duration> positionNotifier = ValueNotifier(Duration.zero);
final ValueNotifier<Duration> durationNotifier = ValueNotifier(Duration.zero);
final ValueNotifier<Duration> bufferedNotifier = ValueNotifier(Duration.zero);
bool _listening = false;
final List<StreamSubscription> _subscriptions = [];
// ---- 播放生命周期标志 ----
bool _handlingCompletion = false;
bool _isChangingTrack = false;
bool _hasStartedCurrentPlayback = false;
bool _isUserSeeking = false;
/// 切歌请求序号:只有最新一次请求有权解锁、写状态。
int _switchEpoch = 0;
/// 缓冲卡死看门狗
static const Duration _stallThreshold = Duration(seconds: 30);
Timer? _stallTimer;
DateTime? _lastStallRecoveryAt;
/// 最近一次播放失败的原因,供 UI 提示;成功播放后自动清空。
String? _lastPlaybackError;
String? get lastPlaybackError => _lastPlaybackError;
int _playbackGeneration = 0;
// ---- 待恢复的播放进度 ----
Duration? _pendingSeekPosition;
void Function(Song)? _onSongChanged;
// ---- Repository ----
final SongDatabase _db = SongDatabase();
final PlaylistRepository _playlistRepo = PlaylistRepository();
// ---- Getter ----
Song? get currentSong => _currentSong;
bool get isPlaying => _isPlaying;
PlayMode get playMode => _playMode;
List<Song> get queue => List.unmodifiable(_queue);
int get currentIndex => _currentIndex;
bool get hasQueue => _queue.isNotEmpty;
String? get currentPlaylistId => _currentPlaylistId;
Duration get position => positionNotifier.value;
Duration get duration => durationNotifier.value;
Duration get bufferedPosition => bufferedNotifier.value;
IconData get playModeIcon {
switch (_playMode) {
case PlayMode.sequential:
return Icons.repeat;
case PlayMode.repeatOne:
return Icons.repeat_one;
case PlayMode.shuffle:
return Icons.shuffle;
}
}
void setOnSongChanged(void Function(Song) callback) {
_onSongChanged = callback;
}
void setHandler(AudioPlayerHandler handler) {
// ⭐ 添加这个方法
_handler = handler;
}
void togglePlayMode() {
switch (_playMode) {
case PlayMode.sequential:
_playMode = PlayMode.repeatOne;
break;
case PlayMode.repeatOne:
_playMode = PlayMode.shuffle;
break;
case PlayMode.shuffle:
_playMode = PlayMode.sequential;
break;
}
notifyListeners();
if (_currentPlaylistId != null) {
_playlistRepo.updatePlaylistPlayMode(_currentPlaylistId!, _playMode);
}
savePlaybackState(); // 模式改变时保存
}
// ════════════════════════════════════════════════════════════
// 队列管理
// ════════════════════════════════════════════════════════════
Future<void> setQueue(List<Song> queue, {int startIndex = 0}) async {
if (queue.isEmpty) {
_clearQueue();
return;
}
_queue = List.from(queue);
_currentIndex = startIndex.clamp(0, _queue.length - 1);
if (_playMode == PlayMode.shuffle) {
_shuffledIndices = List.generate(_queue.length, (i) => i);
_shuffledIndices.shuffle();
_shuffledIndex = _shuffledIndices.indexOf(_currentIndex);
if (_shuffledIndex == -1) {
_shuffledIndex = 0;
_currentIndex = _shuffledIndices[0];
}
} else {
_shuffledIndices = List.generate(_queue.length, (i) => i);
_shuffledIndex = _currentIndex;
}
await _playCurrent();
}
void _clearQueue() {
_queue.clear();
_currentIndex = -1;
_shuffledIndices.clear();
_shuffledIndex = -1;
_currentPlaylistId = null;
stopPlay();
// 清空队列时也清除持久化状态
_db.savePlaybackState(
queueJson: [],
currentIndex: 0,
playMode: 'sequential',
positionMs: 0,
);
}
Future<void> playSong(Song song) async {
_currentPlaylistId = null;
if (_queue.isEmpty || _queue[_currentIndex].id != song.id) {
await setQueue([song], startIndex: 0);
} else {
await _playCurrent();
}
}
// ════════════════════════════════════════════════════════════
// 歌单
// ════════════════════════════════════════════════════════════
Future<void> playPlaylist(String playlistId, {int startIndex = 0}) async {
_currentPlaylistId = playlistId;
final playlist = await _playlistRepo.getPlaylist(playlistId);
if (playlist != null) {
_playMode = playlist.playMode;
}
final songIds = await _playlistRepo.getPlaylistSongIds(playlistId);
if (songIds.isEmpty) return;
final songs = <Song>[];
for (final id in songIds) {
final dbSong = await _db.getSongByPath(id);
if (dbSong != null) {
songs.add(Song(
id: dbSong['remote_path'] as String,
title: dbSong['title'] as String? ?? '',
artist: dbSong['artist'] as String? ?? '未知艺术家',
url: dbSong['remote_path'] as String,
));
} else {
songs.add(Song(
id: id,
title: id.split('/').last.replaceAll(RegExp(r'\.[^.]*$'), ''),
artist: '未知艺术家',
url: id,
));
}
}
if (songs.isNotEmpty) {
await setQueue(songs, startIndex: startIndex);
}
}
Future<Playlist> saveQueueAsPlaylist(String name) async {
final playlist = await _playlistRepo.createPlaylist(name);
for (int i = 0; i < _queue.length; i++) {
final song = _queue[i];
await _playlistRepo.addSong(playlist.id, song.id);
}
return playlist;
}
// ════════════════════════════════════════════════════════════
// ⭐ 播放核心
// ════════════════════════════════════════════════════════════
Future<void> _playCurrent() async {
if (_currentIndex < 0 || _currentIndex >= _queue.length) {
stopPlay();
return;
}
// 「最新请求胜出」:不再因为 _isChangingTrack 为 true 就直接丢弃请求。
// 旧请求会通过 epoch 校验自行放弃后续步骤,避免和新的切歌互相覆盖。
final epoch = ++_switchEpoch;
_isChangingTrack = true;
_handlingCompletion = false;
_hasStartedCurrentPlayback = false;
try {
_playbackGeneration++;
final generation = _playbackGeneration;
final song = _queue[_currentIndex];
_currentSong = song;
_handler?.syncState(song);
_startListening();
notifyListeners();
if (song.url == null || song.url!.isEmpty) {
return;
}
try {
await _playWithHeaders(song.url!);
} catch (e) {
// 播放失败(超时/401/网络中断):必须走到 finally 解锁,
// 并把错误记下来,供上层提示或下一首重试。
_lastPlaybackError = '播放失败: $e';
debugPrint('❌ [AudioService] play failed: $e');
notifyListeners();
return;
}
if (epoch != _switchEpoch) {
debugPrint('↩️ [AudioService] 旧请求已被新的切歌取代,停止后续处理');
return;
}
await _waitForPlaybackStarted();
if (epoch != _switchEpoch) {
debugPrint('↩️ [AudioService] 旧请求已被新的切歌取代,停止后续处理');
return;
}
// ⭐ 如果有待恢复的进度,执行 seek
if (_pendingSeekPosition != null &&
_pendingSeekPosition!.inMilliseconds > 0) {
final pos = _pendingSeekPosition!;
_pendingSeekPosition = null;
await PlaybackService().seek(pos);
positionNotifier.value = pos;
debugPrint('🎯 [AudioService] restored position: $pos');
}
_syncPlayerStateDelayed();
if (_hasStartedCurrentPlayback) {
_lastPlaybackError = null;
_onSongChanged?.call(song);
_loadMetadataForCurrentSong(generation);
}
// 切歌完成后保存状态
await savePlaybackState();
} finally {
// 只有最新的一次切歌才有资格解锁,防止旧请求把新请求的锁提前放掉
if (epoch == _switchEpoch) {
_isChangingTrack = false;
}
}
}
/// 缓冲卡死自愈:重开当前歌曲并 seek 回原位置。
///
/// 触发条件:mpv 的 buffering 持续为 true 超过 [_stallThreshold]。
/// 两次自动恢复之间至少间隔 2 分钟,避免源站长时间不可用时反复重连。
Future<void> _recoverFromStall() async {
if (_isChangingTrack) return;
final song = _currentSong;
if (song == null) return;
final url = song.url;
if (url == null || url.isEmpty) return;
final now = DateTime.now();
if (_lastStallRecoveryAt != null &&
now.difference(_lastStallRecoveryAt!) < const Duration(minutes: 2)) {
debugPrint('🩺 [AudioService] 仍在缓冲,但 2 分钟内已恢复过,交给用户手动切歌');
return;
}
_lastStallRecoveryAt = now;
final resumeAt = positionNotifier.value;
debugPrint(
'🩺 [AudioService] 缓冲超过 ${_stallThreshold.inSeconds}s,尝试重连:${song.title}');
try {
await _playWithHeaders(url);
if (resumeAt > Duration.zero) {
await PlaybackService().seek(resumeAt);
positionNotifier.value = resumeAt;
}
debugPrint('✅ [AudioService] 自动恢复完成');
} catch (e) {
debugPrint('❌ [AudioService] 自动恢复失败: $e');
_lastPlaybackError = '缓冲卡住,自动恢复失败';
notifyListeners();
}
}
Future<void> _waitForPlaybackStarted() async { final player = PlaybackService().player;
if (player.state.playing) {
_hasStartedCurrentPlayback = true;
debugPrint('🎵 [AudioService] playback already started');
return;
}
try {
await player.stream.playing
.where((playing) => playing == true)
.first
.timeout(const Duration(seconds: 3));
_hasStartedCurrentPlayback = true;
debugPrint('🎵 [AudioService] playback started');
} catch (e) {
debugPrint('⚠️ [AudioService] wait for playback timeout: $e');
_hasStartedCurrentPlayback = true;
}
}
Future<void> _playWithHeaders(String url) async {
final headers = await _getAuthHeadersForUrl(url);
await PlaybackService().play(url, headers: headers);
}
Future<Map<String, String>> _getAuthHeadersForUrl(String url) async {
if (url.startsWith('http://') || url.startsWith('https://')) {
try {
final headers = await WebDAVService.instance.getAuthHeaders();
if (headers.isNotEmpty) {
debugPrint('🔑 [AudioService] 已添加认证头到播放请求');
return headers;
}
} catch (e) {
debugPrint('⚠️ [AudioService] 获取认证头失败: $e');
}
}
return {};
}
String _generateSongKey(String url, int fileSize) {
return songKeyFor(url, fileSize);
}
// ════════════════════════════════════════════════════════════
// ⭐ Metadata 加载(四层校验)
// ════════════════════════════════════════════════════════════
Future<void> _loadMetadataForCurrentSong(int generation) async {
if (_currentIndex < 0 || _currentIndex >= _queue.length) return;
final currentIndex = _currentIndex;
final song = _queue[currentIndex];
final songId = song.id;
if (song.url == null || song.url!.isEmpty) {
_onSongChanged?.call(song);
return;
}
try {
final metadata = await MetadataService().getMetadata(
url: song.url!,
fileName: song.title,
fileId: song.id,
);
// 第一层:generation 校验
if (_playbackGeneration != generation) {
debugPrint('⚠️ [AudioService] metadata stale (generation)');
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(
id: song.id,
title: metadata.title.isNotEmpty ? metadata.title : song.title,
artist: metadata.artist.isNotEmpty ? metadata.artist : song.artist,
url: song.url,
artwork: metadata.artwork,
);
_queue[_currentIndex] = updatedSong;
_currentSong = updatedSong;
notifyListeners();
_onSongChanged?.call(updatedSong);
debugPrint(
'✅ [AudioService] metadata updated: ${updatedSong.title} - ${updatedSong.artist} (gen $generation)');
}
} catch (e) {
debugPrint('⚠️ [AudioService] metadata load failed: $e');
_onSongChanged?.call(song);
}
}
Future<void> clearCurrentSongCache() async {
if (_currentSong == null) return;
final song = _currentSong!;
final url = song.url ?? '';
if (url.isEmpty) return;
debugPrint('🗑️ [AudioService] clearing cache for: ${song.title}');
final currentIndex = _currentIndex;
String songKey;
try {
final provider = MetadataService().getProviderForUrl(url);
final file = await provider.getFile(url);
if (file != null && await file.exists()) {
final stat = await file.stat();
songKey = _generateSongKey(url, stat.size);
} else {
songKey = songKeyForUrl(url);
}
} catch (e) {
songKey = songKeyForUrl(url);
}
final db = SongDatabase();
await db.deleteSong(songKey);
await db.deleteCache(songKey);
debugPrint('🗑️ [AudioService] SQLite records deleted: $songKey');
await ArtworkHelper.deleteArtwork(songKey);
debugPrint('🗑️ [AudioService] artwork deleted');
try {
final cacheDir = await getTemporaryDirectory();
final cachePath = '${cacheDir.path}/metadata_${songKeyForUrl(url)}.tmp';
final cacheFile = File(cachePath);
if (await cacheFile.exists()) {
await cacheFile.delete();
debugPrint('🗑️ [AudioService] temp cache file deleted');
}
} catch (e) {
// 忽略
}
await MetadataService().clearCache(song.id);
debugPrint('🗑️ [AudioService] memory cache cleared');
if (currentIndex >= 0 && currentIndex < _queue.length) {
stopPlay();
await _playCurrent();
debugPrint('🔄 [AudioService] song reloaded');
}
}
// ════════════════════════════════════════════════════════════
// ⭐ 上一首 / 下一首(带防并发)
// ════════════════════════════════════════════════════════════
Future<void> next() async {
debugPrint(
'🎵 [next] CALLED: index=$_currentIndex, total=${_queue.length}, isChangingTrack=$_isChangingTrack');
if (_queue.isEmpty) {
debugPrint('⚠️ [next] queue is empty');
return;
}
// 不再因为 _isChangingTrack 拒绝:新请求会取代进行中的切歌(latest wins)
if (_playMode == PlayMode.shuffle) {
if (_shuffledIndices.isEmpty) return;
final nextIdx = (_shuffledIndex + 1) % _shuffledIndices.length;
_shuffledIndex = nextIdx;
_currentIndex = _shuffledIndices[nextIdx];
await _playCurrent();
return;
}
final nextIdx = (_currentIndex + 1) % _queue.length;
_currentIndex = nextIdx;
await _playCurrent();
}
Future<void> previous() async {
if (_queue.isEmpty) return;
// 同 next():最新请求胜出,不拒绝
if (_playMode == PlayMode.shuffle) {
if (_shuffledIndices.isEmpty) return;
final prevIdx = (_shuffledIndex - 1) % _shuffledIndices.length;
if (prevIdx < 0) {
_shuffledIndex = _shuffledIndices.length - 1;
} else {
_shuffledIndex = prevIdx;
}
_currentIndex = _shuffledIndices[_shuffledIndex];
await _playCurrent();
return;
}
final prevIdx = (_currentIndex - 1) % _queue.length;
if (prevIdx < 0) {
_currentIndex = _queue.length - 1;
} else {
_currentIndex = prevIdx;
}
await _playCurrent();
}
// ---- 播放/暂停 ----
void togglePlay() {
if (_currentSong == null) return;
if (_isPlaying) {
PlaybackService().pause();
} else {
PlaybackService().resume();
}
// 保存状态(包括进度)
savePlaybackState();
}
void stopPlay() {
// 停止底层播放器
PlaybackService().stop(); // 新增
_currentSong = null;
_isPlaying = false;
positionNotifier.value = Duration.zero;
durationNotifier.value = Duration.zero;
bufferedNotifier.value = Duration.zero;
_stopListening();
notifyListeners();
savePlaybackState();
}
void seekTo(Duration position) {
_isUserSeeking = true;
PlaybackService().seek(position);
positionNotifier.value = position;
Future.delayed(const Duration(milliseconds: 800), () {
_isUserSeeking = false;
});
// 拖动后保存进度
savePlaybackState();
}
void clearQueue() {
_queue.clear();
_currentIndex = -1;
_shuffledIndices.clear();
_shuffledIndex = -1;
_currentPlaylistId = null;
stopPlay();
notifyListeners();
}
void syncPlayerStateNow() {
final player = PlaybackService().player;
final pos = player.state.position;
final dur = player.state.duration;
final buf = player.state.buffer;
debugPrint(
'🎯 [AudioService] syncPlayerStateNow: pos=$pos, dur=$dur, buf=$buf');
if (pos.inMilliseconds >= 0) {
positionNotifier.value = pos;
}
if (dur.inMilliseconds > 0) {
durationNotifier.value = dur;
}
if (buf.inMilliseconds >= 0) {
bufferedNotifier.value = buf;
}
}
void _syncPlayerStateDelayed() {
syncPlayerStateNow();
Future.delayed(const Duration(milliseconds: 200), () {
syncPlayerStateNow();
});
Future.delayed(const Duration(milliseconds: 500), () {
syncPlayerStateNow();
});
}
// ---- 监听 media_kit 状态 ----
void _startListening() {
if (_listening) return;
_listening = true;
final player = PlaybackService().player;
_subscriptions.add(
player.stream.playing.listen((playing) {
if (_isPlaying != playing) {
_isPlaying = playing;
notifyListeners();
// 播放状态变化时保存进度(暂停时已保存,但播放开始也可保存一次)
if (playing) savePlaybackState();
}
}),
);
_subscriptions.add(
player.stream.position.listen((position) {
if (_isUserSeeking) {
debugPrint('🎯 [AudioService] positionStream ignored: user seeking');
return;
}
positionNotifier.value = position;
}),
);
_subscriptions.add(
player.stream.duration.listen((duration) {
debugPrint('🎯 [AudioService] durationStream: $duration');
if (duration.inMilliseconds > 0) {
durationNotifier.value = duration;
}
}),
);
_subscriptions.add(
player.stream.buffer.listen((buffer) {
bufferedNotifier.value = buffer;
}),
);
// mpv 的错误(401 / 连接失败 / 解码失败)以前没有任何订阅,失败是静默的
_subscriptions.add(
player.stream.error.listen((error) {
debugPrint('❌ [AudioService] player error: $error');
_lastPlaybackError = error;
notifyListeners();
}),
);
// 缓冲状态:定位"流量很高但缓冲为 0"时要靠它
_subscriptions.add(
player.stream.buffering.listen((buffering) {
debugPrint('🌊 [AudioService] buffering=$buffering');
_stallTimer?.cancel();
_stallTimer = null;
if (!buffering) return;
// 持续缓冲超过阈值:多半是网络卡死,主动重连一次当前歌曲
_stallTimer = Timer(_stallThreshold, _recoverFromStall);
}),
);
// ⭐ 完整生命周期校验的 completed 监听
_subscriptions.add(
player.stream.completed.listen((_) {
debugPrint(
'🎵 [completed event] RAW: index=$_currentIndex, isChangingTrack=$_isChangingTrack');
if (_isChangingTrack) {
debugPrint('⚠️ [completed] REJECTED: track switch in progress');
return;
}
if (!_hasStartedCurrentPlayback) {
debugPrint('⚠️ [completed] REJECTED: playback never started');
return;
}
if (_handlingCompletion) {
debugPrint('⚠️ [completed] REJECTED: already handling');
return;
}
final pos = player.state.position;
final dur = player.state.duration;
debugPrint('🎵 [completed] pos=$pos, dur=$dur');
if (dur.inMilliseconds <= 0) {
debugPrint('⚠️ [completed] REJECTED: invalid duration $dur');
return;
}
final diff = dur.inMilliseconds - pos.inMilliseconds;
debugPrint('🎵 [completed] diff=${diff}ms');
if (pos.inMilliseconds < dur.inMilliseconds - 800) {
debugPrint('⚠️ [completed] REJECTED: not at end, diff=${diff}ms');
return;
}
debugPrint('✅ [completed] ACCEPTED: index=$_currentIndex');
_handlingCompletion = true;
_onPlaybackCompleted();
}),
);
}
void _stopListening() {
_listening = false;
for (final subscription in _subscriptions) {
subscription.cancel();
}
_subscriptions.clear();
}
void _onPlaybackCompleted() {
debugPrint(
'🎵 [onPlaybackCompleted] ENTERED: index=$_currentIndex, total=${_queue.length}, mode=$_playMode');
if (_queue.isEmpty) {
debugPrint('⚠️ [onPlaybackCompleted] queue is empty, returning');
_handlingCompletion = false;
return;
}
if (_currentIndex < 0 || _currentIndex >= _queue.length) {
debugPrint(
'⚠️ [onPlaybackCompleted] invalid index: $_currentIndex, total=${_queue.length}');
_handlingCompletion = false;
return;
}
final isLastSong = _currentIndex + 1 >= _queue.length;
if (isLastSong && _playMode != PlayMode.repeatOne) {
debugPrint(
'⚠️ [onPlaybackCompleted] LAST SONG: index=$_currentIndex, mode=$_playMode, no next()');
_handlingCompletion = false;
return;
}
debugPrint(
'🎵 [onPlaybackCompleted] will call next(), index=$_currentIndex');
try {
if (_playMode == PlayMode.repeatOne) {
debugPrint(
'🎵 [onPlaybackCompleted] repeatOne mode, calling _playCurrent()');
_playCurrent();
return;
}
debugPrint('🎵 [onPlaybackCompleted] calling next()');
next();
} finally {
_handlingCompletion = false;
}
}
// ════════════════════════════════════════════════════════════
// ⭐ 播放状态持久化
// ════════════════════════════════════════════════════════════
/// 保存当前播放状态
Future<void> savePlaybackState() async {
if (_queue.isEmpty) {
// 空队列时保存空状态
await _db.savePlaybackState(
queueJson: [],
currentIndex: 0,
playMode: 'sequential',
positionMs: 0,
);
return;
}
final queueJson = _queue.map((song) => song.toJson()).toList();
final modeStr = _playMode == PlayMode.sequential
? 'sequential'
: _playMode == PlayMode.repeatOne
? 'repeat_one'
: 'shuffle';
await _db.savePlaybackState(
queueJson: queueJson,
currentIndex: _currentIndex,
playMode: modeStr,
currentPlaylistId: _currentPlaylistId,
positionMs: positionNotifier.value.inMilliseconds,
);
debugPrint('💾 [AudioService] playback state saved');
}
/// 恢复播放状态
/// 恢复播放状态
Future<bool> restorePlaybackState() async {
final state = await _db.getPlaybackState();
if (state == null) return false;
try {
final queueJson = jsonDecode(state['queue_json'] as String) as List;
if (queueJson.isEmpty) return false;
final queue = queueJson
.map((item) => Song(
id: item['id'] as String,
title: item['title'] as String? ?? '',
artist: item['artist'] as String? ?? '未知艺术家',
url: item['url'] as String?,
artwork: null,
))
.toList();
if (queue.isEmpty) return false;
_queue = queue;
_currentIndex = state['current_index'] as int;
if (_currentIndex >= _queue.length) _currentIndex = 0;
final modeStr = state['play_mode'] as String? ?? 'sequential';
_playMode = modeStr == 'sequential'
? PlayMode.sequential
: modeStr == 'repeat_one'
? PlayMode.repeatOne
: PlayMode.shuffle;
_currentPlaylistId = state['current_playlist_id'] as String?;
// 保存待恢复的进度
final savedPos = state['position_ms'] as int? ?? 0;
_pendingSeekPosition = Duration(milliseconds: savedPos);
final song = _queue[_currentIndex];
_currentSong = song;
_onSongChanged?.call(song);
_handler?.syncState(song); // ⭐ 添加这一行
debugPrint(
'♻️ [AudioService] playback state restored: ${song.title} - ${song.artist}');
// ⭐ 关键:加载音频但不自动播放
await _loadRestoredPlayback();
return true;
} catch (e) {
debugPrint('⚠️ [AudioService] restore playback state failed: $e');
return false;
}
}
/// ⭐ 加载恢复的播放状态(加载音频,不自动播放)
Future<void> _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');
}
@override
void dispose() {
// 先保存再清理
_stallTimer?.cancel();
_stallTimer = null;
savePlaybackState();
_stopListening();
positionNotifier.dispose();
durationNotifier.dispose();
bufferedNotifier.dispose();
PlaybackService().dispose();
super.dispose();
}
}