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.
This commit is contained in:
edde746
2026-07-05 19:26:28 +02:00
parent 05a631415e
commit 422db75b5b
41 changed files with 3353 additions and 269 deletions
+26 -9
View File
@@ -22,7 +22,7 @@ static void* get_opengl_proc_address(void* ctx, const char* name) {
namespace mpv {
MpvPlayer::MpvPlayer() {}
MpvPlayer::MpvPlayer(bool audio_only) : audio_only_(audio_only) {}
MpvPlayer::~MpvPlayer() { Dispose(); }
@@ -41,15 +41,27 @@ bool MpvPlayer::Initialize() {
return false;
}
// Configure mpv for embedded playback.
mpv_set_option_string(mpv_, "vo", "libmpv");
mpv_set_option_string(mpv_, "hwdec", "auto");
if (audio_only_) {
// Music core: no VO, no video decode. vid=no keeps embedded cover art
// from ever becoming a video track, and force-window/audio-display make
// sure mpv never opens a video output for it either.
mpv_set_option_string(mpv_, "vid", "no");
mpv_set_option_string(mpv_, "force-window", "no");
mpv_set_option_string(mpv_, "audio-display", "no");
mpv_set_option_string(mpv_, "gapless-audio", "weak");
} else {
// Configure mpv for embedded playback.
mpv_set_option_string(mpv_, "vo", "libmpv");
mpv_set_option_string(mpv_, "hwdec", "auto");
}
mpv_set_option_string(mpv_, "keep-open", "yes");
// HDR tone mapping
mpv_set_option_string(mpv_, "tone-mapping", "auto");
mpv_set_option_string(mpv_, "target-colorspace-hint", "no");
mpv_set_option_string(mpv_, "hdr-compute-peak", "auto");
if (!audio_only_) {
// HDR tone mapping
mpv_set_option_string(mpv_, "tone-mapping", "auto");
mpv_set_option_string(mpv_, "target-colorspace-hint", "no");
mpv_set_option_string(mpv_, "hdr-compute-peak", "auto");
}
mpv_set_option_string(mpv_, "idle", "yes");
mpv_set_option_string(mpv_, "input-default-bindings", "no");
mpv_set_option_string(mpv_, "input-vo-keyboard", "no");
@@ -71,11 +83,16 @@ bool MpvPlayer::Initialize() {
// Set up event wakeup callback.
mpv_set_wakeup_callback(mpv_, OnMpvWakeup, this);
g_message("MPV: Initialization successful (render context deferred)");
g_message("MPV: Initialization successful (%s)", audio_only_ ? "audio-only" : "render context deferred");
return true;
}
bool MpvPlayer::InitRenderContext() {
if (audio_only_) {
g_warning("MPV: InitRenderContext called on an audio-only player");
return false;
}
if (mpv_gl_) {
return true; // Already created.
}
+9 -3
View File
@@ -34,7 +34,10 @@ using RedrawCallback = std::function<void()>;
/// commands, properties, and event dispatching.
class MpvPlayer {
public:
MpvPlayer();
/// |audio_only| runs mpv as a music core with video disabled entirely:
/// no render context is ever created (InitRenderContext must not be
/// called) and no GL/EGL state is touched.
explicit MpvPlayer(bool audio_only = false);
~MpvPlayer();
/// Initializes the mpv instance and configures options.
@@ -45,6 +48,7 @@ class MpvPlayer {
/// Creates the mpv OpenGL render context.
/// Must be called with a valid GL context current (e.g., from FlTextureGL::populate).
/// Fails on audio-only players.
/// @return true if render context creation succeeded.
bool InitRenderContext();
@@ -60,8 +64,9 @@ class MpvPlayer {
/// Disposes mpv and releases resources.
void Dispose();
/// Returns true if mpv is initialized (has both mpv handle and render context).
bool IsInitialized() const { return mpv_ != nullptr && mpv_gl_ != nullptr; }
/// Returns true if mpv is initialized (has both mpv handle and render
/// context; audio-only players never have a render context).
bool IsInitialized() const { return mpv_ != nullptr && (audio_only_ || mpv_gl_ != nullptr); }
/// Returns true if this player has been disposed.
bool IsDisposed() const { return disposed_.load(); }
@@ -140,6 +145,7 @@ class MpvPlayer {
/// Helper to convert mpv_node to FlValue.
::_FlValue* NodeToFlValue(mpv_node* node);
const bool audio_only_;
mpv_handle* mpv_ = nullptr;
mpv_render_context* mpv_gl_ = nullptr;
+42 -10
View File
@@ -16,6 +16,7 @@ struct _MpvPlugin {
MpvTexture* texture; // owned via GObject ref
gboolean visible;
gboolean initialized;
gboolean audio_only;
};
G_DEFINE_TYPE(MpvPlugin, mpv_plugin, G_TYPE_OBJECT)
@@ -67,31 +68,43 @@ static void mpv_plugin_init(MpvPlugin* self) {
self->initialized = FALSE;
self->texture = nullptr;
self->texture_registrar = nullptr;
self->audio_only = FALSE;
}
MpvPlugin* mpv_plugin_new(FlPluginRegistrar* registrar) {
MpvPlugin* mpv_plugin_new(FlPluginRegistrar* registrar, const gchar* channel_name, gboolean audio_only) {
MpvPlugin* self = MPV_PLUGIN(g_object_new(MPV_PLUGIN_TYPE, nullptr));
self->registrar = FL_PLUGIN_REGISTRAR(g_object_ref(registrar));
self->texture_registrar = fl_plugin_registrar_get_texture_registrar(registrar);
self->player = std::make_unique<mpv::MpvPlayer>();
self->audio_only = audio_only;
// The audio-only core never renders; leaving the texture registrar unset
// makes the GL/texture path structurally unreachable for it.
self->texture_registrar = audio_only ? nullptr : fl_plugin_registrar_get_texture_registrar(registrar);
self->player = std::make_unique<mpv::MpvPlayer>(audio_only);
g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
self->method_channel = fl_method_channel_new(
fl_plugin_registrar_get_messenger(registrar), "com.plezy/mpv_player", FL_METHOD_CODEC(codec));
self->method_channel =
fl_method_channel_new(fl_plugin_registrar_get_messenger(registrar), channel_name, FL_METHOD_CODEC(codec));
fl_method_channel_set_method_call_handler(self->method_channel, mpv_plugin_handle_method_call, self, nullptr);
self->event_channel = fl_event_channel_new(
fl_plugin_registrar_get_messenger(registrar), "com.plezy/mpv_player/events", FL_METHOD_CODEC(codec));
g_autofree gchar* event_channel_name = g_strconcat(channel_name, "/events", nullptr);
self->event_channel =
fl_event_channel_new(fl_plugin_registrar_get_messenger(registrar), event_channel_name, FL_METHOD_CODEC(codec));
return self;
}
// Static reference to keep the plugin alive.
// Static references to keep the plugin instances alive.
static MpvPlugin* g_mpv_plugin = nullptr;
static MpvPlugin* g_mpv_audio_plugin = nullptr;
void mpv_plugin_register_with_registrar(FlPluginRegistrar* registrar) { g_mpv_plugin = mpv_plugin_new(registrar); }
void mpv_plugin_register_with_registrar(FlPluginRegistrar* registrar) {
g_mpv_plugin = mpv_plugin_new(registrar, "com.plezy/mpv_player", FALSE);
}
void mpv_audio_plugin_register_with_registrar(FlPluginRegistrar* registrar) {
g_mpv_audio_plugin = mpv_plugin_new(registrar, "com.plezy/mpv_audio_player", TRUE);
}
/// Method call handler.
static void mpv_plugin_handle_method_call(FlMethodChannel* channel, FlMethodCall* method_call, gpointer user_data) {
@@ -103,7 +116,26 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel, FlMethodCall
g_autoptr(FlMethodResponse) response = nullptr;
if (strcmp(method, "initialize") == 0) {
if (self->initialized && self->texture) {
if (self->audio_only) {
// Audio-only music core: no texture, no render context — mpv runs
// with video disabled entirely (see MpvPlayer). Returns `true`; the
// Dart side only treats int results as texture IDs.
if (!self->initialized) {
if (!self->player || self->player->IsDisposed()) {
self->player = std::make_unique<mpv::MpvPlayer>(/*audio_only=*/true);
}
if (self->player->Initialize()) {
self->player->SetEventCallback([self](FlValue* event) { send_event(self, event); });
self->initialized = TRUE;
}
}
if (self->initialized) {
response = FL_METHOD_RESPONSE(fl_method_success_response_new(fl_value_new_bool(TRUE)));
} else {
response =
FL_METHOD_RESPONSE(fl_method_error_response_new("INIT_FAILED", "Failed to initialize MPV player", nullptr));
}
} else if (self->initialized && self->texture) {
// Already initialized — return existing texture ID
response =
FL_METHOD_RESPONSE(fl_method_success_response_new(fl_value_new_int(mpv_texture_get_id(self->texture))));
+11 -6
View File
@@ -9,21 +9,26 @@
G_BEGIN_DECLS
/// Plugin for MPV video playback on Linux.
/// Plugin for MPV playback on Linux.
///
/// This plugin renders mpv video through Flutter's GPU-accelerated
/// texture pipeline via FlTextureGL.
/// The video instance renders mpv video through Flutter's GPU-accelerated
/// texture pipeline via FlTextureGL. The audio-only instance (music
/// playback) skips all texture/GL work and runs mpv with video disabled.
#define MPV_PLUGIN_TYPE (mpv_plugin_get_type())
G_DECLARE_FINAL_TYPE(MpvPlugin, mpv_plugin, MPV, PLUGIN, GObject)
/// Creates a new MpvPlugin instance.
MpvPlugin* mpv_plugin_new(FlPluginRegistrar* registrar);
/// Creates a new MpvPlugin instance on the given method channel name (the
/// event channel is |channel_name| + "/events").
MpvPlugin* mpv_plugin_new(FlPluginRegistrar* registrar, const gchar* channel_name, gboolean audio_only);
/// Registers the plugin with Flutter.
/// Registers the video plugin with Flutter.
void mpv_plugin_register_with_registrar(FlPluginRegistrar* registrar);
/// Registers the audio-only (music) plugin with Flutter.
void mpv_audio_plugin_register_with_registrar(FlPluginRegistrar* registrar);
G_END_DECLS
#endif // MPV_PLUGIN_H_
+6
View File
@@ -52,6 +52,12 @@ static void my_application_activate(GApplication* application) {
fl_plugin_registry_get_registrar_for_plugin(FL_PLUGIN_REGISTRY(self->flutter_view), "MpvPlugin");
mpv_plugin_register_with_registrar(registrar);
// Register the dedicated audio-only MPV core for music playback (no
// texture/GL work at all).
FlPluginRegistrar* audio_registrar =
fl_plugin_registry_get_registrar_for_plugin(FL_PLUGIN_REGISTRY(self->flutter_view), "MpvAudioPlugin");
mpv_audio_plugin_register_with_registrar(audio_registrar);
gtk_widget_show(GTK_WIDGET(window));
gtk_widget_grab_focus(GTK_WIDGET(self->flutter_view));
}