// 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 '../repositories/playlist_repository.dart'; import '../models/playlist.dart'; import 'webdav_service.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 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 _queue = []; int _currentIndex = -1; List _shuffledIndices = []; int _shuffledIndex = -1; // ---- 当前播放的歌单 ID ---- String? _currentPlaylistId; // ---- 高频进度 ---- final ValueNotifier positionNotifier = ValueNotifier(Duration.zero); final ValueNotifier durationNotifier = ValueNotifier(Duration.zero); final ValueNotifier bufferedNotifier = ValueNotifier(Duration.zero); bool _listening = false; final List _subscriptions = []; // ---- 播放生命周期标志 ---- bool _handlingCompletion = false; bool _isChangingTrack = false; bool _hasStartedCurrentPlayback = false; bool _isUserSeeking = false; 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 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 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 setQueue(List 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 playSong(Song song) async { _currentPlaylistId = null; if (_queue.isEmpty || _queue[_currentIndex].id != song.id) { await setQueue([song], startIndex: 0); } else { await _playCurrent(); } } // ════════════════════════════════════════════════════════════ // 歌单 // ════════════════════════════════════════════════════════════ Future 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 = []; 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 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 _playCurrent() async { if (_isChangingTrack) { debugPrint('⚠️ [AudioService] track switch already running'); return; } if (_currentIndex < 0 || _currentIndex >= _queue.length) { stopPlay(); return; } _isChangingTrack = true; _handlingCompletion = false; _hasStartedCurrentPlayback = false; try { _playbackGeneration++; final generation = _playbackGeneration; final song = _queue[_currentIndex]; _currentSong = song; _startListening(); notifyListeners(); if (song.url == null || song.url!.isEmpty) { return; } await _playWithHeaders(song.url!); 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'); } _syncPlayerStateDelayed(); if (_hasStartedCurrentPlayback) { _onSongChanged?.call(song); _loadMetadataForCurrentSong(generation); } // 切歌完成后保存状态 await savePlaybackState(); } finally { _isChangingTrack = false; } } Future _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 _playWithHeaders(String url) async { final headers = await _getAuthHeadersForUrl(url); await PlaybackService().play(url, headers: headers); } Future> _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, int modifiedTime) { final raw = '$url|$fileSize|$modifiedTime'; return raw.hashCode.toString(); } // ════════════════════════════════════════════════════════════ // ⭐ Metadata 加载(四层校验) // ════════════════════════════════════════════════════════════ Future _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 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, stat.modified.millisecondsSinceEpoch); } else { songKey = url.hashCode.toString(); } } catch (e) { songKey = url.hashCode.toString(); } 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_${url.hashCode}.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 next() async { debugPrint( '🎵 [next] CALLED: index=$_currentIndex, total=${_queue.length}, isChangingTrack=$_isChangingTrack'); if (_queue.isEmpty) { debugPrint('⚠️ [next] queue is empty'); return; } if (_isChangingTrack) { debugPrint('⚠️ [next] REJECTED: track switch in progress'); return; } 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 previous() async { if (_queue.isEmpty) return; if (_isChangingTrack) { debugPrint( '⚠️ [AudioService] previous() ignored: track switch in progress'); return; } 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() { _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; }), ); // ⭐ 完整生命周期校验的 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 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 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); 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 _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() { // 先保存再清理 savePlaybackState(); _stopListening(); positionNotifier.dispose(); durationNotifier.dispose(); bufferedNotifier.dispose(); PlaybackService().dispose(); super.dispose(); } }