// 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 '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 PlaybackService _playback = PlaybackService(); // 当前媒体信息 String? _currentId; String? _currentTitle; String? _currentArtist; Duration _currentPosition = Duration.zero; DateTime _lastPublishTime = DateTime.now(); static const Duration _publishInterval = Duration(milliseconds: 500); String? _currentArtworkPath; bool _isPublishing = false; AudioPlayerHandler() { _bindToPlayer(); } // ═══════════════════════════════════════════════════════════════ // 绑定播放器状态流 // ═══════════════════════════════════════════════════════════════ void _bindToPlayer() { final player = _playback.player; player.stream.playing.listen((playing) { _publishPlaybackState( playing: playing, position: _currentPosition, duration: player.state.duration, ); }); player.stream.position.listen((position) { _currentPosition = position; final now = DateTime.now(); if (now.difference(_lastPublishTime) >= _publishInterval) { _lastPublishTime = now; _publishPlaybackState( playing: player.state.playing, position: position, duration: player.state.duration, onlyPosition: true, ); } }); player.stream.duration.listen((duration) { if (_currentId != null && _currentTitle != null) { _updateMediaItem( id: _currentId!, title: _currentTitle!, artist: _currentArtist!, duration: duration, ); } _publishPlaybackState( playing: player.state.playing, position: _currentPosition, duration: duration, ); }); } // ═══════════════════════════════════════════════════════════════ // 外部同步接口(由 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 _publishPlaybackState({ required bool playing, required Duration position, required Duration duration, bool onlyPosition = false, }) { if (_isPublishing) return; _isPublishing = true; 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 _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, required String artist, Duration? duration, Uint8List? artwork, }) { _currentId = id; _currentTitle = title; _currentArtist = artist; final position = _playback.player.state.position; _handleArtwork(id, artwork).then((artUri) { final current = mediaItem.value; if (current != null && current.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.add(audio_service.MediaItem( id: id, title: title, artist: artist, duration: duration ?? _playback.player.state.duration, extras: {'position': position.inMilliseconds}, )); } Future _handleArtwork(String id, Uint8List? artwork) async { 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); } 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; 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); final player = _playback.player; _publishPlaybackState( playing: player.state.playing, position: _currentPosition, duration: player.state.duration, ); } // ═══════════════════════════════════════════════════════════════ // audio_service 控制命令(全部委托给 PlaybackService) // ═══════════════════════════════════════════════════════════════ @override Future play() async { debugPrint('▶️ [handler] play() called'); await _playback.resume(); } @override Future pause() async { debugPrint('⏸️ [handler] pause() called'); await _playback.pause(); } @override Future stop() async { debugPrint('⏹️ [handler] stop() called'); await _playback.stop(); } @override Future seek(Duration position) async { debugPrint('⏩ [handler] seek() called: $position'); await _playback.seek(position); _currentPosition = position; final player = _playback.player; _publishPlaybackState( playing: player.state.playing, position: position, duration: player.state.duration, onlyPosition: true, ); } @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 (_playback.player.state.playing) { await pause(); } else { await play(); } break; default: if (_playback.player.state.playing) { await pause(); } else { await play(); } } } }