现阶段通知中心播放暂停切换功能已实现,但是上下一曲功能暂时消失,需要后续优化,这个commit是snapshot
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
// lib/audio/playback_state_manager.dart
|
||||
import 'package:audio_service/audio_service.dart' as audio_service;
|
||||
|
||||
class PlaybackStateManager {
|
||||
static final PlaybackStateManager _instance =
|
||||
PlaybackStateManager._internal();
|
||||
factory PlaybackStateManager() => _instance;
|
||||
PlaybackStateManager._internal();
|
||||
|
||||
bool _playing = false;
|
||||
bool get playing => _playing;
|
||||
|
||||
Duration _position = Duration.zero;
|
||||
Duration get position => _position;
|
||||
|
||||
Duration _duration = Duration.zero;
|
||||
Duration get duration => _duration;
|
||||
|
||||
// ⭐ 核心改动:使用 playPause 替代 play/pause 分离
|
||||
audio_service.PlaybackState get playbackState {
|
||||
// ⭐ 固定使用 playPause,不再动态切换
|
||||
final playControl = audio_service.MediaControl(
|
||||
androidIcon: _playing ? 'drawable/ic_pause' : 'drawable/ic_play',
|
||||
label: _playing ? '暂停' : '播放',
|
||||
action: audio_service.MediaAction.playPause, // 固定
|
||||
);
|
||||
|
||||
final controls = [
|
||||
audio_service.MediaControl(
|
||||
androidIcon: 'drawable/ic_previous',
|
||||
label: '上一曲',
|
||||
action: audio_service.MediaAction.skipToPrevious,
|
||||
),
|
||||
playControl,
|
||||
audio_service.MediaControl(
|
||||
androidIcon: 'drawable/ic_next',
|
||||
label: '下一曲',
|
||||
action: audio_service.MediaAction.skipToNext,
|
||||
),
|
||||
];
|
||||
|
||||
return audio_service.PlaybackState(
|
||||
controls: controls,
|
||||
processingState: audio_service.AudioProcessingState.ready,
|
||||
playing: _playing, // ⭐ 这里才是真正的播放状态,通知栏会根据它显示正确的图标
|
||||
androidCompactActionIndices: const [0, 1, 2],
|
||||
updateTime: DateTime.now(),
|
||||
//extras: {'_refresh': DateTime.now().millisecondsSinceEpoch},
|
||||
);
|
||||
}
|
||||
|
||||
// ---- 状态更新方法保持不变 ----
|
||||
void updatePlaying(bool playing) {
|
||||
if (_playing != playing) {
|
||||
_playing = playing;
|
||||
}
|
||||
}
|
||||
|
||||
void updatePosition(Duration position) {
|
||||
_position = position;
|
||||
}
|
||||
|
||||
void updateDuration(Duration duration) {
|
||||
_duration = duration;
|
||||
}
|
||||
|
||||
void reset() {
|
||||
_playing = false;
|
||||
_position = Duration.zero;
|
||||
_duration = Duration.zero;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// lib/audio/player_controller.dart
|
||||
import 'package:media_kit/media_kit.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);
|
||||
}
|
||||
}
|
||||
@@ -287,18 +287,21 @@ class _QTPlayerAppState extends State<QTPlayerApp> {
|
||||
'🎨 [QTPlayerApp] build #$_buildCount ${stopwatch.elapsedMilliseconds}ms');
|
||||
|
||||
// 监听歌曲变化,更新通知栏
|
||||
// main.dart 中 build 方法里的这部分
|
||||
final audioService = context.watch<AudioService>();
|
||||
final handler = context.read<AudioPlayerHandler>();
|
||||
final song = audioService.currentSong;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (song != null) {
|
||||
// ⭐ song 不为空时才调用
|
||||
handler.updateNotification(
|
||||
id: song.id,
|
||||
title: song.title,
|
||||
artist: song.artist,
|
||||
);
|
||||
} else {
|
||||
// 清空通知
|
||||
handler.mediaItem.add(null);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,78 +1,53 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'playback_service.dart';
|
||||
import 'audio_service.dart' as local_audio;
|
||||
// lib/services/audio_player_handler.dart
|
||||
import 'package:flutter/foundation.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'; // 你的业务 AudioService
|
||||
|
||||
class AudioPlayerHandler extends BaseAudioHandler {
|
||||
final PlaybackService _playback = PlaybackService();
|
||||
late final local_audio.AudioService _localAudio;
|
||||
|
||||
// ⭐ 切歌锁,防止重复命令
|
||||
bool _switchingTrack = false;
|
||||
class AudioPlayerHandler extends audio_service.BaseAudioHandler {
|
||||
final PlayerController _player = PlayerController();
|
||||
final PlaybackStateManager _state = PlaybackStateManager();
|
||||
|
||||
AudioPlayerHandler() {
|
||||
_localAudio = local_audio.AudioService();
|
||||
|
||||
// ⭐ 唯一状态来源:media_kit 的 playing 流
|
||||
_playback.player.stream.playing.listen((playing) {
|
||||
debugPrint('🎵 [stream] playing changed: $playing');
|
||||
_updatePlaybackState(playing: playing);
|
||||
_player.playingStream.listen((playing) {
|
||||
_state.updatePlaying(playing);
|
||||
_publishState();
|
||||
});
|
||||
|
||||
// ⭐ 播放完成 → 触发下一首(不直接广播状态)
|
||||
_playback.player.stream.completed.listen((_) {
|
||||
debugPrint('🎵 [stream] completed, auto next');
|
||||
_localAudio.next(); // 触发切歌,playing 流会自然变化
|
||||
// 不调用 _updatePlaybackState()
|
||||
});
|
||||
|
||||
// ⭐ 歌曲变化 → 更新媒体信息(独立于播放状态)
|
||||
// 这里用 duration 变化作为“歌曲已切换”的信号
|
||||
_playback.player.stream.duration.listen((duration) {
|
||||
if (duration.inMilliseconds > 0) {
|
||||
_syncMediaItem();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 状态同步链(只能由 playing 流触发)
|
||||
// ============================================================
|
||||
void _updatePlaybackState({bool? playing}) {
|
||||
final isPlaying = playing ?? _playback.player.state.playing;
|
||||
debugPrint('🎵 _updatePlaybackState: isPlaying=$isPlaying');
|
||||
|
||||
final controls = [
|
||||
MediaControl.skipToPrevious,
|
||||
if (isPlaying) MediaControl.pause else MediaControl.play,
|
||||
MediaControl.skipToNext,
|
||||
];
|
||||
|
||||
debugPrint('🎵 controls: ${controls.map((c) => c.action).join(', ')}');
|
||||
|
||||
playbackState.add(PlaybackState(
|
||||
controls: controls,
|
||||
processingState: AudioProcessingState.ready,
|
||||
playing: isPlaying,
|
||||
androidCompactActionIndices: const [0, 1, 2],
|
||||
_player.positionStream.listen((position) {
|
||||
_state.updatePosition(position);
|
||||
final current = playbackState.value;
|
||||
playbackState.add(audio_service.PlaybackState(
|
||||
controls: current.controls,
|
||||
processingState: current.processingState,
|
||||
playing: current.playing,
|
||||
androidCompactActionIndices: current.androidCompactActionIndices,
|
||||
updateTime: DateTime.now(),
|
||||
));
|
||||
});
|
||||
|
||||
_player.durationStream.listen((duration) {
|
||||
_state.updateDuration(duration);
|
||||
_publishState();
|
||||
});
|
||||
|
||||
_player.completedStream.listen((_) {
|
||||
debugPrint('⏭️ [handler] completed, auto next');
|
||||
AudioService().next();
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 媒体信息同步链(独立于播放状态)
|
||||
// ============================================================
|
||||
void _syncMediaItem() {
|
||||
final song = _localAudio.currentSong;
|
||||
if (song != null) {
|
||||
debugPrint('🎵 [sync] media item: ${song.title}');
|
||||
mediaItem.add(MediaItem(
|
||||
id: song.id,
|
||||
title: song.title,
|
||||
artist: song.artist,
|
||||
duration: _playback.player.state.duration,
|
||||
));
|
||||
}
|
||||
void _publishState() {
|
||||
final state = _state.playbackState;
|
||||
|
||||
debugPrint(
|
||||
'📡 [publish] '
|
||||
'playing=${state.playing}, '
|
||||
'controls=${state.controls.map((e) => e.action).toList()}',
|
||||
);
|
||||
|
||||
playbackState.add(state);
|
||||
}
|
||||
|
||||
void updateNotification({
|
||||
@@ -80,115 +55,77 @@ class AudioPlayerHandler extends BaseAudioHandler {
|
||||
required String title,
|
||||
required String artist,
|
||||
}) {
|
||||
mediaItem.add(MediaItem(
|
||||
debugPrint('📢 [handler] updateNotification: $title');
|
||||
mediaItem.add(audio_service.MediaItem(
|
||||
id: id,
|
||||
title: title,
|
||||
artist: artist,
|
||||
duration: _playback.player.state.duration,
|
||||
duration: _player.duration,
|
||||
));
|
||||
_updatePlaybackState();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 控制命令(只发命令,不宣布状态)
|
||||
// ============================================================
|
||||
// ---- 控制命令(系统回调) ----
|
||||
@override
|
||||
Future<void> play() async {
|
||||
debugPrint('🎵 [audio_service] play() called');
|
||||
await _playback.resume();
|
||||
// ⭐ 不调用 _updatePlaybackState()
|
||||
// 等待 playing 流触发
|
||||
debugPrint('▶️ [handler] play() called');
|
||||
await _player.play();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pause() async {
|
||||
debugPrint('🎵 [audio_service] pause() called');
|
||||
await _playback.pause();
|
||||
// ⭐ 不调用 _updatePlaybackState()
|
||||
debugPrint('⏸️ [handler] pause() called');
|
||||
await _player.pause();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stop() async {
|
||||
debugPrint('🎵 [audio_service] stop() called');
|
||||
await _playback.stop();
|
||||
// ⭐ 不调用 _updatePlaybackState()
|
||||
debugPrint('⏹️ [handler] stop() called');
|
||||
_player.stop();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> seek(Duration position) async {
|
||||
debugPrint('🎵 [audio_service] seek() called: $position');
|
||||
await _playback.seek(position);
|
||||
debugPrint('⏩ [handler] seek() called: $position');
|
||||
_player.seek(position);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> skipToNext() async {
|
||||
if (_switchingTrack) {
|
||||
debugPrint('🎵 skipToNext ignored: already switching');
|
||||
return;
|
||||
}
|
||||
_switchingTrack = true;
|
||||
try {
|
||||
debugPrint('🎵 [audio_service] skipToNext() called');
|
||||
await _localAudio.next();
|
||||
// ⭐ 不调用 _updatePlaybackState()
|
||||
// playing 流和 duration 流会自然触发状态更新
|
||||
} finally {
|
||||
// ⭐ 用真实状态释放锁,不用时间
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
_switchingTrack = false;
|
||||
}
|
||||
debugPrint('⏭️ [handler] skipToNext() called');
|
||||
AudioService().next();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> skipToPrevious() async {
|
||||
if (_switchingTrack) {
|
||||
debugPrint('🎵 skipToPrevious ignored: already switching');
|
||||
return;
|
||||
}
|
||||
_switchingTrack = true;
|
||||
try {
|
||||
debugPrint('🎵 [audio_service] skipToPrevious() called');
|
||||
await _localAudio.previous();
|
||||
} finally {
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
_switchingTrack = false;
|
||||
}
|
||||
debugPrint('⏮️ [handler] skipToPrevious() called');
|
||||
AudioService().previous();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// click():薄路由层,不操作播放器
|
||||
// ============================================================
|
||||
// ⭐ 统一点击处理(仅使用 MediaButton 存在的枚举)
|
||||
@override
|
||||
Future<void> click([MediaButton button = MediaButton.media]) async {
|
||||
debugPrint('🎵 [audio_service] click(): ${button.name} (index: ${button.index})');
|
||||
|
||||
// ⭐ 用 button.name 判断,不用 index
|
||||
final name = button.name.toLowerCase();
|
||||
|
||||
// 路由到标准方法
|
||||
if (name.contains('next') || name == 'next') {
|
||||
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();
|
||||
return;
|
||||
}
|
||||
if (name.contains('previous') || name == 'previous') {
|
||||
break;
|
||||
case audio_service.MediaButton.previous:
|
||||
await skipToPrevious();
|
||||
return;
|
||||
}
|
||||
if (name.contains('pause')) {
|
||||
await pause();
|
||||
return;
|
||||
}
|
||||
if (name.contains('play') || name.contains('media')) {
|
||||
await play();
|
||||
return;
|
||||
}
|
||||
|
||||
// fallback: 根据当前播放状态 toggle
|
||||
debugPrint('🎵 → fallback toggle');
|
||||
if (_playback.player.state.playing) {
|
||||
break;
|
||||
case audio_service.MediaButton.media:
|
||||
// 播放/暂停切换
|
||||
if (_player.isPlaying) {
|
||||
await pause();
|
||||
} else {
|
||||
await play();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// 其他按钮(fastForward, rewind等)忽略或fallback
|
||||
debugPrint('⚠️ [handler] unhandled MediaButton: $button');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,27 +44,27 @@ class PlaybackService {
|
||||
}
|
||||
|
||||
// ---------- 控制 ----------
|
||||
void pause() {
|
||||
Future<void> pause() async {
|
||||
if (_initialized && _player != null) {
|
||||
_player!.pause();
|
||||
await _player!.pause();
|
||||
}
|
||||
}
|
||||
|
||||
void resume() {
|
||||
Future<void> resume() async {
|
||||
if (_initialized && _player != null) {
|
||||
_player!.play();
|
||||
await _player!.play();
|
||||
}
|
||||
}
|
||||
|
||||
void stop() {
|
||||
Future<void> stop() async {
|
||||
if (_initialized && _player != null) {
|
||||
_player!.stop();
|
||||
await _player!.stop();
|
||||
}
|
||||
}
|
||||
|
||||
void seek(Duration position) {
|
||||
Future<void> seek(Duration position) async {
|
||||
if (_initialized && _player != null) {
|
||||
_player!.seek(position);
|
||||
await _player!.seek(position);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user