Files
QingTing-Player/lib/services/audio_player_handler.dart

314 lines
10 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_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<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,
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<Uri?> _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<void> play() async {
debugPrint('▶️ [handler] play() called');
await _playback.resume();
}
@override
Future<void> pause() async {
debugPrint('⏸️ [handler] pause() called');
await _playback.pause();
}
@override
Future<void> stop() async {
debugPrint('⏹️ [handler] stop() called');
await _playback.stop();
}
@override
Future<void> 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<void> skipToNext() async {
debugPrint('⏭️ [handler] skipToNext() called');
AudioService().next();
}
@override
Future<void> skipToPrevious() async {
debugPrint('⏮️ [handler] skipToPrevious() called');
AudioService().previous();
}
@override
Future<void> 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();
}
}
}
}