Files
plezy/lib/services/playback_coordinator.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

56 lines
2.4 KiB
Dart

import '../utils/app_logger.dart';
/// Arbitrates the one-native-player-instance rule between the music engine
/// and the video player.
///
/// Only one native playback core is kept alive at a time: the music
/// service's audio `Player` lives across screens, while the video core only
/// exists while the video player screen is open. The video screen calls
/// [claimVideo] at the very start of its player initialization so a playing
/// music session is fully stopped *and its native core disposed* before the
/// video core is constructed.
class PlaybackCoordinator {
PlaybackCoordinator._();
static final PlaybackCoordinator instance = PlaybackCoordinator._();
Future<void> Function()? _stopMusicSession;
/// Register the active music session's teardown. [stopAndDispose] must
/// stop playback, send final progress, and dispose the audio `Player`
/// before completing. Replaces any previous registration (there is one
/// music service per profile session).
void registerMusicSession({required Future<void> Function() stopAndDispose}) {
_stopMusicSession = stopAndDispose;
}
/// Remove [stopAndDispose] if it is the current registration. Passing the
/// same callback used to register keeps a stale unregister (from an
/// already-replaced session) from tearing down the new one.
void unregisterMusicSession(Future<void> Function() stopAndDispose) {
if (_stopMusicSession == stopAndDispose) _stopMusicSession = null;
}
/// Video playback is about to construct its native core: stop and dispose
/// any live music session first. Completes once the audio core is gone.
Future<void> claimVideo() async {
final stop = _stopMusicSession;
if (stop == null) return;
try {
await stop();
} catch (e, st) {
// The video player must still be able to start; a wedged audio core
// is strictly worse than a leaked stop error.
appLogger.w('PlaybackCoordinator: music session teardown failed', error: e, stackTrace: st);
}
}
/// Music playback is about to construct its audio core. Currently a no-op
/// guard: the video core only exists while the video player screen is
/// open, and music playback cannot be started from inside that screen —
/// leaving it disposes the video core before any music UI is reachable.
/// Kept as an explicit seam so a future "start music over video" flow has
/// a single place to add the reverse teardown.
Future<void> claimMusic() async {}
}