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:
@@ -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.
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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))));
|
||||
|
||||
@@ -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_
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user