// lib/services/audio_player_handler.dart import 'dart:io'; 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'; class AudioPlayerHandler extends audio_service.BaseAudioHandler { final PlayerController _player = PlayerController(); final PlaybackStateManager _state = PlaybackStateManager(); String? _currentId; String? _currentTitle; String? _currentArtist; Duration _currentPosition = Duration.zero; DateTime _lastPublishTime = DateTime.now(); static const Duration _publishInterval = Duration(milliseconds: 500); // ⭐ 缓存 artwork 文件路径,避免重复写入 String? _currentArtworkPath; AudioPlayerHandler() { _player.playingStream.listen((playing) { _state.updatePlaying(playing); _publishState(); }); _player.positionStream.listen((position) { _currentPosition = position; _state.updatePosition(position); final now = DateTime.now(); if (now.difference(_lastPublishTime) >= _publishInterval) { _lastPublishTime = now; // ⭐ 只保留 _publishStateOnly() _publishStateOnly(); } }); _player.durationStream.listen((duration) { debugPrint('🎯 [durationStream] duration=$duration'); _state.updateDuration(duration); if (_currentId != null && _currentTitle != null) { _updateMediaItem( id: _currentId!, title: _currentTitle!, artist: _currentArtist!, duration: duration, ); } _publishState(); }); } // ---- 发布状态 ---- 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, }, )); } void _publishStateOnly() { final current = playbackState.value; 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, }, )); } // ---- 更新媒体信息 ---- void _updateMediaItem({ required String id, required String title, required String artist, 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'); // ⭐ 异步处理 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, artist: current.artist, duration: current.duration, artUri: artUri, extras: current.extras, )); } }); // 先推送不带封面图的 MediaItem(让 UI 尽快显示) mediaItem.add(audio_service.MediaItem( id: id, title: title, artist: artist, duration: duration ?? _player.duration, extras: {'position': position.inMilliseconds}, )); } /// 处理封面图:保存到本地并生成 content URI Future _handleArtwork(String id, Uint8List? artwork) async { if (artwork == null || artwork.isEmpty) { _currentArtworkPath = null; 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 && existingBytes.hashCode == artwork.hashCode) { _currentArtworkPath = path; 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'); return null; } } // ---- 外部接口 ---- void updateNotification({ required String id, required String title, required String artist, Uint8List? artwork, }) { _updateMediaItem( id: id, title: title, artist: artist, artwork: artwork, ); _publishState(); } // ---- 控制命令 ---- @override Future play() async { debugPrint('▶️ [handler] play() called'); _player.play(); _publishState(); } @override Future pause() async { debugPrint('⏸️ [handler] pause() called'); _player.pause(); _publishState(); } @override Future stop() async { debugPrint('⏹️ [handler] stop() called'); _player.stop(); _publishState(); } @override Future seek(Duration position) async { debugPrint('⏩ [handler] seek() called: $position'); await _player.seek(position); _currentPosition = position; _publishStateOnly(); } @override Future skipToNext() async { debugPrint('⏭️ [handler] skipToNext() called'); AudioService().next(); } @override Future skipToPrevious() async { debugPrint('⏮️ [handler] skipToPrevious() called'); AudioService().previous(); } @override Future click( [audio_service.MediaButton button = audio_service.MediaButton.media]) async { debugPrint('🎯 [handler] click() called with button: ${button.name}'); switch (button) { case audio_service.MediaButton.next: await skipToNext(); break; case audio_service.MediaButton.previous: await skipToPrevious(); break; case audio_service.MediaButton.media: if (_player.isPlaying) { await pause(); } else { await play(); } break; default: if (_player.isPlaying) { await pause(); } else { await play(); } } } }