Files
plezy/lib/services/music/music_playback_service.dart
T
edde746 4816e3928f fix(player): skip relative to the position a jump landed on
A coalesced key-repeat skip pins its target so a slow backend cannot make
the next press rebase off a position the seek has not reached yet. Nothing
retired that pin when something else moved the playhead, so for the ten
seconds it survived, a skip taken after a timeline tap, a chapter jump, an
OS media control or a peer sync resumed from the superseded target and threw
the user back across their own jump.

Publish every playhead movement on the player and retire the pin whenever
the announced destination is not the accumulator's own commit. Overlapping
seeks and backend-chosen relocations arbitrate by which operation the
backend accepted, so a request that was merely asked for cannot speak for
where the playhead ended up.

close #1819
2026-08-07 08:43:48 +02:00

158 lines
5.8 KiB
Dart
Raw 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.
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. The player keeps
/// artist/playlist/mix provenance stable, while album and ad-hoc queues use
/// the active track's album for the "Playing from …" line.
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 of the session source. Used directly for stable
/// artist/playlist/mix provenance labels.
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.
abstract class MusicPlaybackService extends ChangeNotifier {
MediaItem? get currentTrack;
MusicPlaybackStatus get status;
bool get isPlaying => status == MusicPlaybackStatus.playing;
Duration? get duration;
Duration get position;
Stream<Duration> get positionStream;
/// Mirrors `Player.streams.playheadJump`: something is moving the playhead
/// discontinuously, to this position, or to somewhere only the backend knows
/// when null. Request-time intent, not an observed landing.
/// Consumers coalescing their own relative seeks use it to drop a pending
/// target something else superseded (#1819).
Stream<Duration?> get playheadJumpStream => const Stream<Duration?>.empty();
/// 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;
/// Claims the latest user intent to replace playback after asynchronous
/// queue construction. Callers must check [isPlayIntentCurrent] before
/// committing fetched tracks.
int beginPlayIntent();
/// Whether [intent] is still the latest playback-replacement request.
bool isPlayIntentCurrent(int intent);
/// Changes whenever a queue session starts or stops. Asynchronous enqueue
/// actions use this to avoid appending fetched tracks to a newer session.
int get queueSessionRevision;
/// Start a new queue from [tracks], optionally at [startTrack] (defaults
/// to the first track). [shuffle] anchors [startTrack] first and shuffles
/// the rest after it; with no [startTrack] the whole list shuffles, so the
/// queue opens on a random track rather than always the first one (#1811).
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);
/// Music playback volume, 0100. Preview updates are exposed separately so
/// a slider does not notify every service consumer on each drag event.
double get volume;
ValueListenable<double> get volumeListenable;
Future<void> setVolume(double volume, {bool persist = true});
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;
/// The duration the timed sleep timer was armed with (for marking the
/// chosen preset); null in end-of-track mode or when inactive.
Duration? get sleepTimerDuration;
/// 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);
}