✨ Audio service
This commit is contained in:
@@ -1,26 +1,200 @@
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:media_kit/media_kit.dart' as media_kit;
|
||||
import 'package:groovybox/data/db.dart';
|
||||
|
||||
class AudioHandler {
|
||||
final Player _player;
|
||||
class AudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
|
||||
final media_kit.Player _player;
|
||||
List<MediaItem> _queue = [];
|
||||
int _queueIndex = 0;
|
||||
|
||||
AudioHandler() : _player = Player() {
|
||||
AudioHandler() : _player = media_kit.Player() {
|
||||
// Configure for audio
|
||||
// _player.setPlaylistMode(PlaylistMode.loop); // Optional
|
||||
|
||||
// Listen to player state changes and broadcast to audio_service
|
||||
_player.stream.playing.listen((playing) {
|
||||
_broadcastPlaybackState();
|
||||
});
|
||||
|
||||
_player.stream.position.listen((position) {
|
||||
_broadcastPlaybackState();
|
||||
});
|
||||
|
||||
_player.stream.duration.listen((duration) {
|
||||
_broadcastPlaybackState();
|
||||
});
|
||||
|
||||
_player.stream.playlist.listen((playlist) {
|
||||
if (playlist.medias.isNotEmpty) {
|
||||
final currentIndex = playlist.index;
|
||||
if (currentIndex >= 0 && currentIndex < _queue.length) {
|
||||
_queueIndex = currentIndex;
|
||||
mediaItem.add(_queue[_queueIndex]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Player get player => _player;
|
||||
media_kit.Player get player => _player;
|
||||
|
||||
// AudioService callbacks
|
||||
@override
|
||||
Future<void> play() => _player.play();
|
||||
|
||||
@override
|
||||
Future<void> pause() => _player.pause();
|
||||
|
||||
@override
|
||||
Future<void> stop() => _player.stop();
|
||||
|
||||
@override
|
||||
Future<void> seek(Duration position) => _player.seek(position);
|
||||
|
||||
Future<void> setSource(String path) async {
|
||||
await _player.open(Media(path));
|
||||
@override
|
||||
Future<void> skipToNext() async {
|
||||
if (_queueIndex < _queue.length - 1) {
|
||||
_queueIndex++;
|
||||
await _player.jump(_queueIndex);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> openPlaylist(List<Media> medias, {int initialIndex = 0}) async {
|
||||
await _player.open(Playlist(medias, index: initialIndex), play: true);
|
||||
@override
|
||||
Future<void> skipToPrevious() async {
|
||||
if (_queueIndex > 0) {
|
||||
_queueIndex--;
|
||||
await _player.jump(_queueIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> skipToQueueItem(int index) async {
|
||||
if (index >= 0 && index < _queue.length) {
|
||||
_queueIndex = index;
|
||||
await _player.jump(index);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> addQueueItem(MediaItem mediaItem) async {
|
||||
_queue.add(mediaItem);
|
||||
queue.add(_queue);
|
||||
await _updatePlaylist();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> insertQueueItem(int index, MediaItem mediaItem) async {
|
||||
if (index >= 0 && index <= _queue.length) {
|
||||
_queue.insert(index, mediaItem);
|
||||
queue.add(_queue);
|
||||
await _updatePlaylist();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> removeQueueItem(MediaItem mediaItem) async {
|
||||
_queue.remove(mediaItem);
|
||||
queue.add(_queue);
|
||||
await _updatePlaylist();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> updateQueue(List<MediaItem> queue) async {
|
||||
_queue = List.from(queue);
|
||||
this.queue.add(_queue);
|
||||
await _updatePlaylist();
|
||||
}
|
||||
|
||||
Future<void> _updatePlaylist() async {
|
||||
final medias = _queue.map((item) => media_kit.Media(item.id)).toList();
|
||||
if (medias.isNotEmpty) {
|
||||
await _player.open(media_kit.Playlist(medias, index: _queueIndex));
|
||||
}
|
||||
}
|
||||
|
||||
void _broadcastPlaybackState() {
|
||||
final playing = _player.state.playing;
|
||||
final position = _player.state.position;
|
||||
final duration = _player.state.duration;
|
||||
|
||||
playbackState.add(
|
||||
PlaybackState(
|
||||
controls: [
|
||||
MediaControl.skipToPrevious,
|
||||
playing ? MediaControl.pause : MediaControl.play,
|
||||
MediaControl.stop,
|
||||
MediaControl.skipToNext,
|
||||
],
|
||||
systemActions: const {
|
||||
MediaAction.seek,
|
||||
MediaAction.seekForward,
|
||||
MediaAction.seekBackward,
|
||||
},
|
||||
androidCompactActionIndices: const [0, 1, 3],
|
||||
processingState: AudioProcessingState.ready,
|
||||
playing: playing,
|
||||
updatePosition: position,
|
||||
bufferedPosition: duration,
|
||||
speed: 1.0,
|
||||
queueIndex: _queueIndex,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// New methods that accept Track objects with proper metadata
|
||||
Future<void> playTrack(Track track) async {
|
||||
final mediaItem = _trackToMediaItem(track);
|
||||
await updateQueue([mediaItem]);
|
||||
}
|
||||
|
||||
Future<void> playTracks(List<Track> tracks, {int initialIndex = 0}) async {
|
||||
final mediaItems = tracks.map(_trackToMediaItem).toList();
|
||||
_queueIndex = initialIndex;
|
||||
await updateQueue(mediaItems);
|
||||
}
|
||||
|
||||
MediaItem _trackToMediaItem(Track track) {
|
||||
return MediaItem(
|
||||
id: track.path,
|
||||
album: track.album,
|
||||
title: track.title,
|
||||
artist: track.artist,
|
||||
duration: track.duration != null
|
||||
? Duration(milliseconds: track.duration!)
|
||||
: null,
|
||||
artUri: track.artUri != null ? Uri.file(track.artUri!) : null,
|
||||
);
|
||||
}
|
||||
|
||||
// Legacy methods for backward compatibility
|
||||
Future<void> setSource(String path) async {
|
||||
final mediaItem = MediaItem(
|
||||
id: path,
|
||||
album: 'Unknown Album',
|
||||
title: _extractTitleFromPath(path),
|
||||
artist: 'Unknown Artist',
|
||||
);
|
||||
await updateQueue([mediaItem]);
|
||||
}
|
||||
|
||||
Future<void> openPlaylist(
|
||||
List<media_kit.Media> medias, {
|
||||
int initialIndex = 0,
|
||||
}) async {
|
||||
final mediaItems = medias.map((media) {
|
||||
return MediaItem(
|
||||
id: media.uri,
|
||||
album: 'Unknown Album',
|
||||
title: _extractTitleFromPath(media.uri),
|
||||
artist: 'Unknown Artist',
|
||||
);
|
||||
}).toList();
|
||||
|
||||
_queueIndex = initialIndex;
|
||||
await updateQueue(mediaItems);
|
||||
}
|
||||
|
||||
String _extractTitleFromPath(String path) {
|
||||
return path.split('/').last.split('.').first;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
|
||||
@@ -1,11 +1,30 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:audio_service/audio_service.dart' as audio_service;
|
||||
import 'logic/audio_handler.dart';
|
||||
import 'providers/audio_provider.dart';
|
||||
import 'ui/shell.dart';
|
||||
|
||||
void main() {
|
||||
late AudioHandler _audioHandler;
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
MediaKit.ensureInitialized();
|
||||
|
||||
// Initialize AudioService
|
||||
_audioHandler = await audio_service.AudioService.init(
|
||||
builder: () => AudioHandler(),
|
||||
config: const audio_service.AudioServiceConfig(
|
||||
androidNotificationChannelId: 'dev.solsynth.groovybox.channel.audio',
|
||||
androidNotificationChannelName: 'GroovyBox Audio',
|
||||
androidNotificationOngoing: true,
|
||||
),
|
||||
);
|
||||
|
||||
// Set the audio handler for the provider
|
||||
setAudioHandler(_audioHandler);
|
||||
|
||||
runApp(const ProviderScope(child: MyApp()));
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,15 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'audio_provider.g.dart';
|
||||
|
||||
// This should be set after AudioService.init in main.dart
|
||||
late AudioHandler _audioHandler;
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
AudioHandler audioHandler(Ref ref) {
|
||||
final handler = AudioHandler();
|
||||
ref.onDispose(() => handler.dispose());
|
||||
return handler;
|
||||
return _audioHandler;
|
||||
}
|
||||
|
||||
// Function to set the audio handler after initialization
|
||||
void setAudioHandler(AudioHandler handler) {
|
||||
_audioHandler = handler;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'package:groovybox/data/db.dart';
|
||||
import 'package:groovybox/data/playlist_repository.dart';
|
||||
import 'package:groovybox/providers/audio_provider.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:media_kit/media_kit.dart' hide Track;
|
||||
|
||||
class AlbumDetailScreen extends HookConsumerWidget {
|
||||
final AlbumData album;
|
||||
@@ -102,8 +101,7 @@ class AlbumDetailScreen extends HookConsumerWidget {
|
||||
|
||||
void _playAlbum(WidgetRef ref, List<Track> tracks, {int initialIndex = 0}) {
|
||||
final audioHandler = ref.read(audioHandlerProvider);
|
||||
final medias = tracks.map((t) => Media(t.path)).toList();
|
||||
audioHandler.openPlaylist(medias, initialIndex: initialIndex);
|
||||
audioHandler.playTracks(tracks, initialIndex: initialIndex);
|
||||
}
|
||||
|
||||
String _formatDuration(int? durationMs) {
|
||||
|
||||
@@ -300,8 +300,7 @@ class LibraryScreen extends HookConsumerWidget {
|
||||
),
|
||||
onTap: () {
|
||||
final audio = ref.read(audioHandlerProvider);
|
||||
audio.setSource(track.path);
|
||||
audio.play();
|
||||
audio.playTrack(track);
|
||||
},
|
||||
onLongPress: () {
|
||||
// Enter selection mode
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:groovybox/data/db.dart';
|
||||
import 'package:groovybox/data/playlist_repository.dart';
|
||||
import 'package:groovybox/providers/audio_provider.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:media_kit/media_kit.dart' hide Track, Playlist;
|
||||
|
||||
class PlaylistDetailScreen extends HookConsumerWidget {
|
||||
final Playlist playlist;
|
||||
@@ -114,8 +113,7 @@ class PlaylistDetailScreen extends HookConsumerWidget {
|
||||
int initialIndex = 0,
|
||||
}) {
|
||||
final audioHandler = ref.read(audioHandlerProvider);
|
||||
final medias = tracks.map((t) => Media(t.path)).toList();
|
||||
audioHandler.openPlaylist(medias, initialIndex: initialIndex);
|
||||
audioHandler.playTracks(tracks, initialIndex: initialIndex);
|
||||
}
|
||||
|
||||
String _formatDuration(int? durationMs) {
|
||||
|
||||
Reference in New Issue
Block a user