Files
QingTing-Player/lib/services/audio_service.dart
lxh2875931338 974dee2995 revert 99917bac87
revert Core Freeze:核心链路功能已经完成设计,后续进入beta功能开发模式,接下来会在UI部分微调后,进入首个Release版本(1.1.0-Release)
2026-09-02 10:15:54 +08:00

884 lines
26 KiB
Dart
Raw Permalink 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 '../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<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;
// ---- 高频进度 ----
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 _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 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 (_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<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, int modifiedTime) {
final raw = '$url|$fileSize|$modifiedTime';
return raw.hashCode.toString();
}
// ════════════════════════════════════════════════════════════
// ⭐ 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, 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<void> 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<void> 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<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);
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() {
// 先保存再清理
savePlaybackState();
_stopListening();
positionNotifier.dispose();
durationNotifier.dispose();
bufferedNotifier.dispose();
PlaybackService().dispose();
super.dispose();
}
}