Files
plezy/lib/services/music/music_playback_service.dart
T
edde746 422db75b5b feat(music): mpv audio playback engine with gapless queue service
Audio-only mpv core on every platform (dedicated
com.plezy/mpv_audio_player channels): parameterized android/windows/
linux mpv plugins and a new apple MpvAudioPlayerCore, all skipping
video/window paths (vid=no, audio-display=no, gapless-audio=weak).
MusicPlaybackService drives an in-memory queue with shuffle/repeat,
file-loaded-event gapless arming (property edges coalesce and the
android bridge drops them), per-track progress reporting, OS media
controls, audio focus, sleep timer, and error auto-skip.
PlaybackCoordinator enforces one live native player: starting video
disposes the audio core first.
2026-07-05 19:26:28 +02:00

245 lines
6.6 KiB
Dart

import 'package:flutter/foundation.dart';
import '../../media/lyrics.dart';
import '../../media/media_item.dart';
/// Repeat behavior of the music queue.
enum MusicRepeatMode { off, all, one }
/// Coarse playback state of the music session.
enum MusicPlaybackStatus { idle, loading, playing, paused, error }
/// What kind of container playback was started from — drives the
/// "Playing from …" line in the player UI.
enum MusicPlayContextKind { album, artist, playlist, mix, tracks }
/// Provenance of the current queue (album/artist/playlist/instant mix).
class MusicPlayContext {
/// Backend id of the source container, when it has one (instant mixes
/// don't).
final String? id;
/// Display title ("Playing from {title}").
final String title;
final MusicPlayContextKind kind;
const MusicPlayContext({this.id, required this.title, required this.kind});
}
/// Backend-neutral music playback session: owns the audio `Player`, the
/// queue (shuffle/repeat), OS media-session feed, and progress reporting.
///
/// UI consumes this via `context.watch<MusicPlaybackService>()` — it is
/// registered per profile session (see `profile_session_screen.dart`) so a
/// profile switch tears the session down. [notifyListeners] fires only on
/// discrete changes (track, status, queue shape, modes) — progress bars
/// subscribe to [positionStream] instead.
///
/// [StubMusicPlaybackService] is registered until the playback engine lands;
/// UI gates transport affordances on [isAvailable].
abstract class MusicPlaybackService extends ChangeNotifier {
/// False on the stub — playback affordances should render disabled or
/// fall back to a "not supported yet" notice.
bool get isAvailable;
MediaItem? get currentTrack;
MusicPlaybackStatus get status;
bool get isPlaying => status == MusicPlaybackStatus.playing;
Duration? get duration;
Duration get position;
Stream<Duration> get positionStream;
/// Full queue in playback order (shuffle already applied).
List<MediaItem> get queue;
/// Index of [currentTrack] within [queue]; -1 when idle.
int get currentIndex;
MusicPlayContext? get playContext;
bool get shuffled;
MusicRepeatMode get repeatMode;
/// Playback failures the UI should surface (snackbar); the service already
/// handles recovery (skip / stop) itself.
Stream<Object> get errors;
/// Start a new queue from [tracks], optionally at [startTrack] (defaults
/// to the first track). [shuffle] shuffles with the start track anchored
/// first.
Future<void> playFromList({
required List<MediaItem> tracks,
MediaItem? startTrack,
required MusicPlayContext playContext,
bool shuffle = false,
});
/// Fetch an instant mix seeded from [seed] and play it.
Future<void> playInstantMix(MediaItem seed);
Future<void> play();
Future<void> pause();
Future<void> togglePlayPause();
/// Advance to the next queue entry (respecting repeat mode).
Future<void> next();
/// Restart the current track when more than a few seconds in, otherwise
/// step to the previous queue entry.
Future<void> previous();
Future<void> seek(Duration position);
void setRepeatMode(MusicRepeatMode mode);
void toggleShuffle();
/// Jump playback to queue index [index].
Future<void> jumpTo(int index);
void removeAt(int index);
void reorder(int from, int to);
/// Insert after the current track.
void addNext(List<MediaItem> tracks);
void addToEnd(List<MediaItem> tracks);
/// Drop everything after the current track.
void clearUpcoming();
/// Stop playback and clear the session (mini-player disappears).
Future<void> stop();
/// Whether a sleep timer (timed or end-of-track) is armed.
bool get sleepTimerActive;
/// When the timed sleep timer fires; null in end-of-track mode or when
/// inactive.
DateTime? get sleepTimerEndsAt;
/// Whether the sleep timer pauses at the end of the current track instead
/// of after a fixed duration.
bool get sleepTimerEndOfTrack;
/// Arm the sleep timer: a fixed [duration], or [endOfTrack] to pause when
/// the current track finishes. Pass `null` with `endOfTrack: false` to
/// cancel. Fires as a pause (session stays); cancelled by [stop].
void setSleepTimer(Duration? duration, {bool endOfTrack = false});
/// Lyrics for [track] (defaults to the current track's backend). Delegates
/// to `MediaServerClient.fetchLyrics`; null = none available.
Future<Lyrics?> fetchLyrics(MediaItem track);
}
/// No-op placeholder bound while the playback engine is not wired yet (or
/// on platforms where it failed to initialize). Keeps every UI consumer
/// null-safe without per-call-site feature checks.
class StubMusicPlaybackService extends MusicPlaybackService {
@override
bool get isAvailable => false;
@override
MediaItem? get currentTrack => null;
@override
MusicPlaybackStatus get status => MusicPlaybackStatus.idle;
@override
Duration? get duration => null;
@override
Duration get position => Duration.zero;
@override
Stream<Duration> get positionStream => const Stream.empty();
@override
List<MediaItem> get queue => const [];
@override
int get currentIndex => -1;
@override
MusicPlayContext? get playContext => null;
@override
bool get shuffled => false;
@override
MusicRepeatMode get repeatMode => MusicRepeatMode.off;
@override
Stream<Object> get errors => const Stream.empty();
@override
Future<void> playFromList({
required List<MediaItem> tracks,
MediaItem? startTrack,
required MusicPlayContext playContext,
bool shuffle = false,
}) async {}
@override
Future<void> playInstantMix(MediaItem seed) async {}
@override
Future<void> play() async {}
@override
Future<void> pause() async {}
@override
Future<void> togglePlayPause() async {}
@override
Future<void> next() async {}
@override
Future<void> previous() async {}
@override
Future<void> seek(Duration position) async {}
@override
void setRepeatMode(MusicRepeatMode mode) {}
@override
void toggleShuffle() {}
@override
Future<void> jumpTo(int index) async {}
@override
void removeAt(int index) {}
@override
void reorder(int from, int to) {}
@override
void addNext(List<MediaItem> tracks) {}
@override
void addToEnd(List<MediaItem> tracks) {}
@override
void clearUpcoming() {}
@override
Future<void> stop() async {}
@override
bool get sleepTimerActive => false;
@override
DateTime? get sleepTimerEndsAt => null;
@override
bool get sleepTimerEndOfTrack => false;
@override
void setSleepTimer(Duration? duration, {bool endOfTrack = false}) {}
@override
Future<Lyrics?> fetchLyrics(MediaItem track) async => null;
}