Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40d4af322b |
@@ -1,32 +0,0 @@
|
||||
// lib/audio/player_controller.dart
|
||||
import '../services/playback_service.dart'; // ⭐ 修正路径
|
||||
|
||||
class PlayerController {
|
||||
static final PlayerController _instance = PlayerController._internal();
|
||||
factory PlayerController() => _instance;
|
||||
PlayerController._internal();
|
||||
|
||||
final PlaybackService _playback = PlaybackService();
|
||||
|
||||
// ---- 状态流(只读) ----
|
||||
Stream<bool> get playingStream => _playback.player.stream.playing;
|
||||
Stream<Duration> get positionStream => _playback.player.stream.position;
|
||||
Stream<Duration> get durationStream => _playback.player.stream.duration;
|
||||
Stream<void> get completedStream => _playback.player.stream.completed;
|
||||
|
||||
// ---- 当前状态快照 ----
|
||||
bool get isPlaying => _playback.player.state.playing;
|
||||
Duration get position => _playback.player.state.position;
|
||||
Duration get duration => _playback.player.state.duration;
|
||||
|
||||
// ---- 控制命令 ----
|
||||
Future<void> play() => _playback.resume();
|
||||
Future<void> pause() => _playback.pause();
|
||||
Future<void> stop() => _playback.stop();
|
||||
Future<void> seek(Duration position) => _playback.seek(position);
|
||||
|
||||
// ---- 加载歌曲 ----
|
||||
void load(String url, {Map<String, String>? headers}) {
|
||||
_playback.play(url, headers: headers);
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,8 @@ void main() async {
|
||||
try {
|
||||
_audioHandler = AudioPlayerHandler();
|
||||
debugPrint('⏱️ T0.62 AudioPlayerHandler created');
|
||||
// ⭐ 将 Handler 注入到 AudioService
|
||||
AudioService().setHandler(_audioHandler!);
|
||||
} catch (e, st) {
|
||||
debugPrint('❌ AudioPlayerHandler failed: $e');
|
||||
debugPrint('$st');
|
||||
|
||||
@@ -4,49 +4,62 @@ import 'dart:typed_data';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:audio_service/audio_service.dart' as audio_service;
|
||||
import '../audio/player_controller.dart';
|
||||
import '../audio/playback_state_manager.dart';
|
||||
import '../services/audio_service.dart';
|
||||
import '../utils/file_provider_utils.dart';
|
||||
import 'dart:convert';
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'playback_service.dart';
|
||||
import 'audio_service.dart';
|
||||
import '../utils/file_provider_utils.dart';
|
||||
|
||||
/// 音频服务处理器:连接 media_kit 播放引擎与 Android 系统媒体会话
|
||||
/// 职责:发布播放状态到系统通知栏,接收系统媒体按钮事件
|
||||
/// 它不拥有 Player 实例,所有播放控制均委托给 PlaybackService
|
||||
class AudioPlayerHandler extends audio_service.BaseAudioHandler {
|
||||
final PlayerController _player = PlayerController();
|
||||
final PlaybackStateManager _state = PlaybackStateManager();
|
||||
final PlaybackService _playback = PlaybackService();
|
||||
|
||||
// 当前媒体信息
|
||||
String? _currentId;
|
||||
String? _currentTitle;
|
||||
String? _currentArtist;
|
||||
|
||||
Duration _currentPosition = Duration.zero;
|
||||
DateTime _lastPublishTime = DateTime.now();
|
||||
static const Duration _publishInterval = Duration(milliseconds: 500);
|
||||
|
||||
// ⭐ 缓存 artwork 文件路径,避免重复写入
|
||||
String? _currentArtworkPath;
|
||||
bool _isPublishing = false;
|
||||
|
||||
AudioPlayerHandler() {
|
||||
_player.playingStream.listen((playing) {
|
||||
_state.updatePlaying(playing);
|
||||
_publishState();
|
||||
_bindToPlayer();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// 绑定播放器状态流
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
void _bindToPlayer() {
|
||||
final player = _playback.player;
|
||||
|
||||
player.stream.playing.listen((playing) {
|
||||
_publishPlaybackState(
|
||||
playing: playing,
|
||||
position: _currentPosition,
|
||||
duration: player.state.duration,
|
||||
);
|
||||
});
|
||||
|
||||
_player.positionStream.listen((position) {
|
||||
player.stream.position.listen((position) {
|
||||
_currentPosition = position;
|
||||
_state.updatePosition(position);
|
||||
|
||||
final now = DateTime.now();
|
||||
if (now.difference(_lastPublishTime) >= _publishInterval) {
|
||||
_lastPublishTime = now;
|
||||
// ⭐ 只保留 _publishStateOnly()
|
||||
_publishStateOnly();
|
||||
_publishPlaybackState(
|
||||
playing: player.state.playing,
|
||||
position: position,
|
||||
duration: player.state.duration,
|
||||
onlyPosition: true,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
_player.durationStream.listen((duration) {
|
||||
debugPrint('🎯 [durationStream] duration=$duration');
|
||||
_state.updateDuration(duration);
|
||||
player.stream.duration.listen((duration) {
|
||||
if (_currentId != null && _currentTitle != null) {
|
||||
_updateMediaItem(
|
||||
id: _currentId!,
|
||||
@@ -55,45 +68,93 @@ class AudioPlayerHandler extends audio_service.BaseAudioHandler {
|
||||
duration: duration,
|
||||
);
|
||||
}
|
||||
_publishState();
|
||||
_publishPlaybackState(
|
||||
playing: player.state.playing,
|
||||
position: _currentPosition,
|
||||
duration: duration,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ---- 发布状态 ----
|
||||
void _publishState() {
|
||||
final state = _state.playbackState;
|
||||
playbackState.add(audio_service.PlaybackState(
|
||||
controls: state.controls,
|
||||
processingState: state.processingState,
|
||||
playing: state.playing,
|
||||
androidCompactActionIndices: state.androidCompactActionIndices,
|
||||
updatePosition: _currentPosition,
|
||||
updateTime: DateTime.now(),
|
||||
systemActions: const {
|
||||
audio_service.MediaAction.seek,
|
||||
},
|
||||
));
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// 外部同步接口(由 AudioService 调用)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
void syncState(Song? song) {
|
||||
if (song == null) {
|
||||
_currentId = null;
|
||||
_currentTitle = null;
|
||||
_currentArtist = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_updateMediaItem(
|
||||
id: song.id,
|
||||
title: song.title,
|
||||
artist: song.artist,
|
||||
artwork: song.artwork,
|
||||
);
|
||||
|
||||
final player = _playback.player;
|
||||
_publishPlaybackState(
|
||||
playing: player.state.playing,
|
||||
position: _currentPosition,
|
||||
duration: player.state.duration,
|
||||
);
|
||||
}
|
||||
|
||||
void _publishStateOnly() {
|
||||
final current = playbackState.value;
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// 状态发布
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
void _publishPlaybackState({
|
||||
required bool playing,
|
||||
required Duration position,
|
||||
required Duration duration,
|
||||
bool onlyPosition = false,
|
||||
}) {
|
||||
if (_isPublishing) return;
|
||||
_isPublishing = true;
|
||||
|
||||
playbackState.add(audio_service.PlaybackState(
|
||||
controls: current.controls.isNotEmpty
|
||||
? current.controls
|
||||
: _state.playbackState.controls,
|
||||
processingState: _state.playbackState.processingState,
|
||||
playing: current.playing,
|
||||
androidCompactActionIndices: current.androidCompactActionIndices,
|
||||
updatePosition: _currentPosition,
|
||||
updateTime: DateTime.now(),
|
||||
systemActions: const {
|
||||
audio_service.MediaAction.seek,
|
||||
},
|
||||
));
|
||||
try {
|
||||
final controls = _buildControls(playing);
|
||||
final state = audio_service.PlaybackState(
|
||||
controls: controls,
|
||||
processingState: duration.inMilliseconds > 0
|
||||
? audio_service.AudioProcessingState.ready
|
||||
: audio_service.AudioProcessingState.idle,
|
||||
playing: playing,
|
||||
updatePosition: position,
|
||||
updateTime: DateTime.now(),
|
||||
systemActions: const {audio_service.MediaAction.seek},
|
||||
);
|
||||
playbackState.add(state);
|
||||
} finally {
|
||||
_isPublishing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 更新媒体信息 ----
|
||||
List<audio_service.MediaControl> _buildControls(bool playing) {
|
||||
return [
|
||||
const audio_service.MediaControl(
|
||||
androidIcon: 'drawable/ic_previous',
|
||||
label: '上一曲',
|
||||
action: audio_service.MediaAction.skipToPrevious,
|
||||
),
|
||||
audio_service.MediaControl(
|
||||
androidIcon: playing ? 'drawable/ic_pause' : 'drawable/ic_play',
|
||||
label: playing ? '暂停' : '播放',
|
||||
action: audio_service.MediaAction.playPause,
|
||||
),
|
||||
const audio_service.MediaControl(
|
||||
androidIcon: 'drawable/ic_next',
|
||||
label: '下一曲',
|
||||
action: audio_service.MediaAction.skipToNext,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// 媒体信息更新
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
void _updateMediaItem({
|
||||
required String id,
|
||||
required String title,
|
||||
@@ -101,21 +162,15 @@ class AudioPlayerHandler extends audio_service.BaseAudioHandler {
|
||||
Duration? duration,
|
||||
Uint8List? artwork,
|
||||
}) {
|
||||
debugPrint(
|
||||
'📢 [handler] _updateMediaItem: artwork is ${artwork != null ? 'not null (${artwork.length} bytes)' : 'null'}');
|
||||
_currentId = id;
|
||||
_currentTitle = title;
|
||||
_currentArtist = artist;
|
||||
final position = _player.position;
|
||||
|
||||
debugPrint('📢 [handler] updateMediaItem: $title - $artist');
|
||||
final position = _playback.player.state.position;
|
||||
|
||||
// ⭐ 异步处理 artwork(不阻塞主流程)
|
||||
_handleArtwork(id, artwork).then((artUri) {
|
||||
// 如果 artUri 变化,重新推送 MediaItem
|
||||
final current = mediaItem.value;
|
||||
if (current != null && current.artUri != artUri) {
|
||||
debugPrint('📢 [handler] updating artUri: $artUri');
|
||||
mediaItem.add(audio_service.MediaItem(
|
||||
id: current.id,
|
||||
title: current.title,
|
||||
@@ -127,38 +182,28 @@ class AudioPlayerHandler extends audio_service.BaseAudioHandler {
|
||||
}
|
||||
});
|
||||
|
||||
// 先推送不带封面图的 MediaItem(让 UI 尽快显示)
|
||||
mediaItem.add(audio_service.MediaItem(
|
||||
id: id,
|
||||
title: title,
|
||||
artist: artist,
|
||||
duration: duration ?? _player.duration,
|
||||
duration: duration ?? _playback.player.state.duration,
|
||||
extras: {'position': position.inMilliseconds},
|
||||
));
|
||||
}
|
||||
|
||||
/// 处理封面图:保存到本地并生成 content URI
|
||||
Future<Uri?> _handleArtwork(String id, Uint8List? artwork) async {
|
||||
if (artwork == null || artwork.isEmpty) {
|
||||
_currentArtworkPath = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (artwork == null || artwork.isEmpty) return null;
|
||||
try {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
final artworkDir = Directory('${dir.path}/artworks');
|
||||
if (!await artworkDir.exists()) {
|
||||
await artworkDir.create(recursive: true);
|
||||
}
|
||||
|
||||
// 使用 md5 生成安全的文件名
|
||||
final bytes = utf8.encode(id);
|
||||
final digest = md5.convert(bytes);
|
||||
final fileName = '$digest.jpg';
|
||||
final path = '${artworkDir.path}/$fileName';
|
||||
final file = File(path);
|
||||
|
||||
// 检查文件是否已存在
|
||||
if (await file.exists()) {
|
||||
final existingBytes = await file.readAsBytes();
|
||||
if (existingBytes.length == artwork.length &&
|
||||
@@ -167,12 +212,8 @@ class AudioPlayerHandler extends audio_service.BaseAudioHandler {
|
||||
return await FileProviderUtils.getContentUri(file);
|
||||
}
|
||||
}
|
||||
|
||||
// 写入新文件
|
||||
await file.writeAsBytes(artwork);
|
||||
_currentArtworkPath = path;
|
||||
debugPrint('📢 [handler] artwork saved: $path');
|
||||
|
||||
return await FileProviderUtils.getContentUri(file);
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ [handler] artwork handling failed: $e');
|
||||
@@ -180,50 +221,54 @@ class AudioPlayerHandler extends audio_service.BaseAudioHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 外部接口 ----
|
||||
void updateNotification({
|
||||
required String id,
|
||||
required String title,
|
||||
required String artist,
|
||||
Uint8List? artwork,
|
||||
}) {
|
||||
_updateMediaItem(
|
||||
id: id,
|
||||
title: title,
|
||||
artist: artist,
|
||||
artwork: artwork,
|
||||
_updateMediaItem(id: id, title: title, artist: artist, artwork: artwork);
|
||||
final player = _playback.player;
|
||||
_publishPlaybackState(
|
||||
playing: player.state.playing,
|
||||
position: _currentPosition,
|
||||
duration: player.state.duration,
|
||||
);
|
||||
_publishState();
|
||||
}
|
||||
|
||||
// ---- 控制命令 ----
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// audio_service 控制命令(全部委托给 PlaybackService)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
@override
|
||||
Future<void> play() async {
|
||||
debugPrint('▶️ [handler] play() called');
|
||||
_player.play();
|
||||
_publishState();
|
||||
await _playback.resume();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pause() async {
|
||||
debugPrint('⏸️ [handler] pause() called');
|
||||
_player.pause();
|
||||
_publishState();
|
||||
await _playback.pause();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stop() async {
|
||||
debugPrint('⏹️ [handler] stop() called');
|
||||
_player.stop();
|
||||
_publishState();
|
||||
await _playback.stop();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> seek(Duration position) async {
|
||||
debugPrint('⏩ [handler] seek() called: $position');
|
||||
await _player.seek(position);
|
||||
await _playback.seek(position);
|
||||
_currentPosition = position;
|
||||
_publishStateOnly();
|
||||
final player = _playback.player;
|
||||
_publishPlaybackState(
|
||||
playing: player.state.playing,
|
||||
position: position,
|
||||
duration: player.state.duration,
|
||||
onlyPosition: true,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -251,14 +296,14 @@ class AudioPlayerHandler extends audio_service.BaseAudioHandler {
|
||||
await skipToPrevious();
|
||||
break;
|
||||
case audio_service.MediaButton.media:
|
||||
if (_player.isPlaying) {
|
||||
if (_playback.player.state.playing) {
|
||||
await pause();
|
||||
} else {
|
||||
await play();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (_player.isPlaying) {
|
||||
if (_playback.player.state.playing) {
|
||||
await pause();
|
||||
} else {
|
||||
await play();
|
||||
|
||||
@@ -13,6 +13,7 @@ import '../utils/artwork_helper.dart';
|
||||
import '../repositories/playlist_repository.dart';
|
||||
import '../models/playlist.dart';
|
||||
import 'webdav_service.dart';
|
||||
import 'audio_player_handler.dart';
|
||||
|
||||
enum PlayMode {
|
||||
sequential,
|
||||
@@ -62,6 +63,9 @@ class AudioService extends ChangeNotifier {
|
||||
// ---- 当前播放的歌单 ID ----
|
||||
String? _currentPlaylistId;
|
||||
|
||||
// ⭐ 插入位置:在现有成员变量之后,方法之前
|
||||
AudioPlayerHandler? _handler; // ⭐ 添加这一行
|
||||
|
||||
// ---- 高频进度 ----
|
||||
final ValueNotifier<Duration> positionNotifier = ValueNotifier(Duration.zero);
|
||||
final ValueNotifier<Duration> durationNotifier = ValueNotifier(Duration.zero);
|
||||
@@ -115,6 +119,11 @@ class AudioService extends ChangeNotifier {
|
||||
_onSongChanged = callback;
|
||||
}
|
||||
|
||||
void setHandler(AudioPlayerHandler handler) {
|
||||
// ⭐ 添加这个方法
|
||||
_handler = handler;
|
||||
}
|
||||
|
||||
void togglePlayMode() {
|
||||
switch (_playMode) {
|
||||
case PlayMode.sequential:
|
||||
@@ -264,6 +273,8 @@ class AudioService extends ChangeNotifier {
|
||||
final song = _queue[_currentIndex];
|
||||
_currentSong = song;
|
||||
|
||||
_handler?.syncState(song);
|
||||
|
||||
_startListening();
|
||||
notifyListeners();
|
||||
|
||||
@@ -547,6 +558,8 @@ class AudioService extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void stopPlay() {
|
||||
// 停止底层播放器
|
||||
PlaybackService().stop(); // 新增
|
||||
_currentSong = null;
|
||||
_isPlaying = false;
|
||||
positionNotifier.value = Duration.zero;
|
||||
@@ -554,7 +567,6 @@ class AudioService extends ChangeNotifier {
|
||||
bufferedNotifier.value = Duration.zero;
|
||||
_stopListening();
|
||||
notifyListeners();
|
||||
// 停止时也保存一次
|
||||
savePlaybackState();
|
||||
}
|
||||
|
||||
@@ -821,6 +833,8 @@ class AudioService extends ChangeNotifier {
|
||||
_currentSong = song;
|
||||
_onSongChanged?.call(song);
|
||||
|
||||
_handler?.syncState(song); // ⭐ 添加这一行
|
||||
|
||||
debugPrint(
|
||||
'♻️ [AudioService] playback state restored: ${song.title} - ${song.artist}');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user