Files
QingTing-Player/lib/services/audio_service.dart
T
lxh2875931338 2b33c17aab fix(playback): 增加进度看门狗,兜住不产生 buffering 事件的半开连接
公网链路实测(清远->华为云->香港CN2->openresty->OpenList HTTPS)中
出现过 tcp: ffurl_read 错误但未触发 buffering=true 的情况,
仅靠缓冲事件无法覆盖这类"连接半死"的卡顿。

- 每 10s 检查一次播放进度;播放中且 30s 未推进 → 触发自愈(重开 + seek 回原位)
- 切歌中/暂停时重置基准,避免误触发
- 与 buffering 看门狗共用 2 分钟节流

验证:公网路径连续播放 65s,无 false positive,进度正常推进。
2026-10-23 21:18:02 +08:00

1021 lines
31 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;
/// 进度看门狗:某些卡死不会触发 buffering 事件(例如 CDN 半开连接),
/// 用「播放中进度长时间不动」兜底。
Timer? _progressTimer;
Duration _lastObservedPosition = Duration.zero;
DateTime _lastProgressAt = DateTime.now();
/// 最近一次播放失败的原因,供 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;
}
}
}
/// 进度看门狗:播放中但进度 30 秒没动,说明流已经卡死(半开连接/CDN 卡住)。
void _checkPlaybackProgress() {
if (_isChangingTrack || !_isPlaying) {
// 切歌中或暂停时不参与判断,重置基准
_lastObservedPosition = positionNotifier.value;
_lastProgressAt = DateTime.now();
return;
}
final pos = positionNotifier.value;
if (pos != _lastObservedPosition) {
_lastObservedPosition = pos;
_lastProgressAt = DateTime.now();
return;
}
if (DateTime.now().difference(_lastProgressAt) >= _stallThreshold) {
debugPrint('🩺 [AudioService] 播放进度卡在 $pos 超过 '
'${_stallThreshold.inSeconds}s,触发自愈');
_lastProgressAt = DateTime.now();
_recoverFromStall();
}
}
/// 缓冲卡死自愈:重开当前歌曲并 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);
}),
);
// 进度看门狗:有些半开连接不会产生 buffering=true,
// 但播放进度会一直停在原地,这里兜住这种情况。
_progressTimer?.cancel();
_lastProgressAt = DateTime.now();
_progressTimer = Timer.periodic(
const Duration(seconds: 10),
(_) => _checkPlaybackProgress(),
);
// ⭐ 完整生命周期校验的 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;
_progressTimer?.cancel();
_progressTimer = null;
savePlaybackState();
_stopListening();
positionNotifier.dispose();
durationNotifier.dispose();
bufferedNotifier.dispose();
PlaybackService().dispose();
super.dispose();
}
}