feat: use FlTextureGL for Linux mpv video rendering

This commit is contained in:
edde746
2026-02-17 08:55:29 +01:00
parent 544fccf82d
commit 2bc2c0a0ec
12 changed files with 376 additions and 436 deletions
-4
View File
@@ -70,10 +70,6 @@ set_target_properties(${BINARY_NAME}
)
# Bundle SDL3 from source so the build works on systems without libsdl3-dev
# (e.g. arm64 Linux where no distro package exists yet).
set(BUNDLE_SDL3 ON CACHE BOOL "" FORCE)
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
+1
View File
@@ -11,6 +11,7 @@ add_executable(${BINARY_NAME}
"my_application.cc"
"mpv/mpv_player.cc"
"mpv/mpv_plugin.cc"
"mpv/mpv_texture.cc"
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
)
+40 -71
View File
@@ -43,19 +43,11 @@ MpvPlayer::~MpvPlayer() {
Dispose();
}
bool MpvPlayer::Initialize(GtkGLArea* gl_area) {
bool MpvPlayer::Initialize() {
if (mpv_) {
return true; // Already initialized.
}
gl_area_ = gl_area;
// Check if GL area is realized
if (!gtk_widget_get_realized(GTK_WIDGET(gl_area))) {
g_warning("MPV: GL area not realized yet");
return false;
}
// MPV requires C locale for numeric formatting
std::setlocale(LC_NUMERIC, "C");
@@ -67,11 +59,11 @@ bool MpvPlayer::Initialize(GtkGLArea* gl_area) {
}
// Configure mpv for embedded playback.
mpv_set_option_string(mpv_, "vo", "libmpv"); // Render via mpv_render_context_render()
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 - ensures HDR content is properly converted to SDR
// 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");
@@ -81,7 +73,7 @@ bool MpvPlayer::Initialize(GtkGLArea* gl_area) {
mpv_set_option_string(mpv_, "osc", "no");
mpv_set_option_string(mpv_, "terminal", "no");
// Default to warn-level logging; Dart side can raise to "v" if debug logging is enabled.
// Default to warn-level logging
mpv_request_log_messages(mpv_, "warn");
// Initialize mpv.
@@ -93,12 +85,20 @@ bool MpvPlayer::Initialize(GtkGLArea* gl_area) {
return false;
}
// Make the GL context current.
gtk_gl_area_make_current(gl_area_);
if (gtk_gl_area_get_error(gl_area_) != nullptr) {
g_warning("MPV: Failed to make GL context current");
mpv_terminate_destroy(mpv_);
mpv_ = nullptr;
// Set up event wakeup callback.
mpv_set_wakeup_callback(mpv_, OnMpvWakeup, this);
g_message("MPV: Initialization successful (render context deferred)");
return true;
}
bool MpvPlayer::InitRenderContext() {
if (mpv_gl_) {
return true; // Already created.
}
if (!mpv_) {
g_warning("MPV: Cannot create render context - mpv not initialized");
return false;
}
@@ -115,45 +115,37 @@ bool MpvPlayer::Initialize(GtkGLArea* gl_area) {
{MPV_RENDER_PARAM_INVALID, nullptr},
};
err = mpv_render_context_create(&mpv_gl_, mpv_, params);
int err = mpv_render_context_create(&mpv_gl_, mpv_, params);
if (err < 0) {
g_warning("MPV: mpv_render_context_create() failed: %s",
mpv_error_string(err));
mpv_terminate_destroy(mpv_);
mpv_ = nullptr;
return false;
}
// Set up event wakeup callback.
mpv_set_wakeup_callback(mpv_, OnMpvWakeup, this);
// Set up render update callback.
mpv_render_context_set_update_callback(mpv_gl_, OnMpvRenderUpdate, this);
g_message("MPV: Initialization successful");
g_message("MPV: Render context created successfully");
return true;
}
void MpvPlayer::Dispose() {
// Lock mutex to prevent race with OnMpvWakeup callbacks.
// This ensures no new event processing starts during dispose.
std::lock_guard<std::mutex> lock(callback_mutex_);
// Guard against multiple dispose calls (double-free protection)
if (disposed_.exchange(true)) {
return; // Already disposed
return;
}
// Cancel pending async commands
{
std::lock_guard<std::mutex> cmd_lock(pending_commands_mutex_);
for (auto& pair : pending_commands_) {
if (pair.second) pair.second(-1); // Call with error
if (pair.second) pair.second(-1);
}
pending_commands_.clear();
}
// Clear mpv callbacks BEFORE freeing to prevent new callbacks being scheduled
// Clear mpv callbacks BEFORE freeing
if (mpv_gl_) {
mpv_render_context_set_update_callback(mpv_gl_, nullptr, nullptr);
}
@@ -167,20 +159,18 @@ void MpvPlayer::Dispose() {
event_source_id_ = 0;
}
// Now safe to free render context
if (mpv_gl_) {
mpv_render_context_free(mpv_gl_);
mpv_gl_ = nullptr;
}
// And terminate mpv
if (mpv_) {
mpv_terminate_destroy(mpv_);
mpv_ = nullptr;
}
observed_properties_.clear();
gl_area_ = nullptr;
redraw_callback_ = nullptr;
}
void MpvPlayer::Command(const std::vector<std::string>& args) {
@@ -210,7 +200,6 @@ void MpvPlayer::CommandAsync(const std::vector<std::string>& args,
}
c_args.push_back(nullptr);
// Generate unique request ID and store callback
uint64_t request_id;
{
std::lock_guard<std::mutex> lock(pending_commands_mutex_);
@@ -218,10 +207,8 @@ void MpvPlayer::CommandAsync(const std::vector<std::string>& args,
pending_commands_[request_id] = std::move(callback);
}
// mpv_command_async returns immediately
int result = mpv_command_async(mpv_, request_id, c_args.data());
if (result < 0) {
// Submission failed, complete immediately with error
std::lock_guard<std::mutex> lock(pending_commands_mutex_);
auto it = pending_commands_.find(request_id);
if (it != pending_commands_.end()) {
@@ -253,7 +240,6 @@ void MpvPlayer::ObserveProperty(const std::string& name,
int id) {
if (disposed_ || !mpv_) return;
// Check if already observing.
if (observed_properties_.find(name) != observed_properties_.end()) {
return;
}
@@ -288,7 +274,7 @@ void MpvPlayer::Render(int width, int height, int fbo) {
.internal_format = 0,
};
int flip_y = 1;
int flip_y = 0;
mpv_render_param params[] = {
{MPV_RENDER_PARAM_OPENGL_FBO, &mpv_fbo},
@@ -312,29 +298,9 @@ void MpvPlayer::SetEventCallback(EventCallback callback) {
event_callback_ = std::move(callback);
}
void MpvPlayer::RequestRedraw() {
if (disposed_) return;
// Only queue one idle handler at a time
bool expected = false;
if (!needs_redraw_.compare_exchange_strong(expected, true)) {
return; // Already have a pending redraw
}
if (gl_area_) {
GtkGLArea* area = gl_area_;
g_idle_add_full(
GDK_PRIORITY_REDRAW,
[](gpointer data) -> gboolean {
GtkGLArea* area = static_cast<GtkGLArea*>(data);
if (GTK_IS_GL_AREA(area)) {
gtk_gl_area_queue_render(area);
}
return G_SOURCE_REMOVE;
},
area,
nullptr);
}
void MpvPlayer::SetRedrawCallback(RedrawCallback callback) {
std::lock_guard<std::mutex> lock(callback_mutex_);
redraw_callback_ = std::move(callback);
}
void MpvPlayer::SetLogLevel(const std::string& level) {
@@ -345,18 +311,13 @@ void MpvPlayer::SetLogLevel(const std::string& level) {
void MpvPlayer::OnMpvWakeup(void* ctx) {
auto* player = static_cast<MpvPlayer*>(ctx);
// Don't schedule if already disposed (atomic check for early exit)
if (player->disposed_) return;
// Schedule event processing on the main thread.
g_idle_add_full(
G_PRIORITY_HIGH_IDLE,
[](gpointer data) -> gboolean {
auto* player = static_cast<MpvPlayer*>(data);
// Check disposed - atomic ensures we see updated value.
// Don't lock mutex here - ProcessEvents() calls SendPropertyChange/SendEvent
// which lock callback_mutex_, causing deadlock if we hold it here.
if (!player->disposed_ && player->mpv_) {
player->ProcessEvents();
}
@@ -368,8 +329,18 @@ void MpvPlayer::OnMpvWakeup(void* ctx) {
void MpvPlayer::OnMpvRenderUpdate(void* ctx) {
auto* player = static_cast<MpvPlayer*>(ctx);
// RequestRedraw already checks disposed_
player->RequestRedraw();
if (player->disposed_) return;
bool expected = false;
if (!player->needs_redraw_.compare_exchange_strong(expected, true)) {
return;
}
// Call the redraw callback to notify MpvTexture via the registrar
std::lock_guard<std::mutex> lock(player->callback_mutex_);
if (player->redraw_callback_) {
player->redraw_callback_();
}
}
bool MpvPlayer::ProcessEvents() {
@@ -391,7 +362,6 @@ bool MpvPlayer::ProcessEvents() {
void MpvPlayer::HandleMpvEvent(mpv_event* event) {
switch (event->event_id) {
case MPV_EVENT_COMMAND_REPLY: {
// Handle async command completion
uint64_t request_id = event->reply_userdata;
CommandCallback callback;
{
@@ -403,7 +373,6 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) {
}
}
if (callback) {
// Call callback on main thread
int error = event->error;
g_idle_add(
[](gpointer data) -> gboolean {
+23 -30
View File
@@ -25,6 +25,9 @@ namespace mpv {
/// Note: FlValue* is passed from the global namespace, not mpv namespace.
using EventCallback = std::function<void(::_FlValue*)>;
/// Callback for requesting a redraw (called from mpv render update thread).
using RedrawCallback = std::function<void()>;
/// Wrapper for libmpv that handles initialization, OpenGL rendering,
/// commands, properties, and event dispatching.
class MpvPlayer {
@@ -32,65 +35,59 @@ class MpvPlayer {
MpvPlayer();
~MpvPlayer();
/// Initializes mpv with OpenGL rendering context.
/// Must be called from the GTK main thread after GL context is available.
/// @param gl_area The GtkGLArea widget for rendering.
/// Initializes the mpv instance and configures options.
/// Does NOT create the render context — call InitRenderContext() later
/// when an OpenGL context is available.
/// @return true if initialization succeeded.
bool Initialize(GtkGLArea* gl_area);
bool Initialize();
/// Creates the mpv OpenGL render context.
/// Must be called with a valid GL context current (e.g., from FlTextureGL::populate).
/// @return true if render context creation succeeded.
bool InitRenderContext();
/// Returns true if the render context has been created.
bool HasRenderContext() const { return mpv_gl_ != nullptr; }
/// Disposes mpv and releases resources.
void Dispose();
/// Returns true if mpv is initialized.
/// 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 handle exists (even without render context).
bool HasMpvHandle() const { return mpv_ != nullptr; }
/// Executes an mpv command.
/// @param args Command arguments (e.g., ["loadfile", "url", "replace"]).
void Command(const std::vector<std::string>& args);
/// Callback type for async command completion.
using CommandCallback = std::function<void(int error)>;
/// Executes an mpv command asynchronously to prevent UI blocking.
/// The callback is called on the main thread when the command completes.
/// @param args Command arguments.
/// @param callback Callback called with error code (0 = success).
void CommandAsync(const std::vector<std::string>& args, CommandCallback callback);
/// Sets an mpv property by name.
/// @param name Property name.
/// @param value Property value as string.
void SetProperty(const std::string& name, const std::string& value);
/// Gets an mpv property value by name.
/// @param name Property name.
/// @return Property value as string, or empty if not found.
std::string GetProperty(const std::string& name);
/// Observes an mpv property for changes.
/// Changes will be reported via the event callback.
/// @param name Property name to observe.
/// @param format Format type ("string", "flag", "int64", "double", "node").
/// @param id Property ID assigned by Dart for compact event encoding.
void ObserveProperty(const std::string& name, const std::string& format,
int id);
/// Renders a frame to the current OpenGL context.
/// Must be called from the GTK render callback.
/// @param width Viewport width.
/// @param height Viewport height.
/// @param fbo Framebuffer object to render into (0 for default).
/// Renders a frame to the specified FBO.
void Render(int width, int height, int fbo = 0);
/// Reports that the mouse has moved.
/// This is used to show/hide the cursor.
void ReportMouseMove(int x, int y);
/// Sets the event callback for property changes and events.
void SetEventCallback(EventCallback callback);
/// Returns the GtkGLArea widget.
GtkGLArea* GetGLArea() const { return gl_area_; }
/// Sets the redraw callback (called when mpv has a new frame ready).
void SetRedrawCallback(RedrawCallback callback);
/// Returns true if a redraw is needed.
bool NeedsRedraw() const { return needs_redraw_.load(); }
@@ -98,9 +95,6 @@ class MpvPlayer {
/// Clears the redraw flag.
void ClearRedrawFlag() { needs_redraw_.store(false); }
/// Request a redraw.
void RequestRedraw();
/// Sets the MPV log message level (e.g., "warn", "v", "debug").
void SetLogLevel(const std::string& level);
@@ -112,7 +106,6 @@ class MpvPlayer {
static void OnMpvRenderUpdate(void* ctx);
/// Processes pending mpv events.
/// @return true to keep processing, false if shutdown.
bool ProcessEvents();
/// Handles a single mpv event.
@@ -129,11 +122,11 @@ class MpvPlayer {
mpv_handle* mpv_ = nullptr;
mpv_render_context* mpv_gl_ = nullptr;
GtkGLArea* gl_area_ = nullptr;
std::atomic<bool> needs_redraw_{false};
std::atomic<bool> disposed_{false};
EventCallback event_callback_;
RedrawCallback redraw_callback_;
std::mutex callback_mutex_;
uint64_t next_reply_userdata_ = 1;
+65 -220
View File
@@ -1,21 +1,18 @@
#include "mpv_plugin.h"
#include "mpv_texture.h"
#include <cstring>
/// Plugin structure definition.
struct _MpvPlugin {
GObject parent_instance;
FlPluginRegistrar* registrar;
FlMethodChannel* method_channel;
FlEventChannel* event_channel;
FlBasicMessageChannel* event_message_channel;
GtkOverlay* overlay;
GtkGLArea* gl_area;
GtkWidget* flutter_view;
FlTextureRegistrar* texture_registrar;
std::unique_ptr<mpv::MpvPlayer> player;
MpvTexture* texture; // owned via GObject ref
gboolean visible;
gboolean initialized;
};
@@ -26,12 +23,17 @@ G_DEFINE_TYPE(MpvPlugin, mpv_plugin, G_TYPE_OBJECT)
static void mpv_plugin_handle_method_call(FlMethodChannel* channel,
FlMethodCall* method_call,
gpointer user_data);
static gboolean on_gl_render(GtkGLArea* area,
GdkGLContext* context,
gpointer user_data);
static void on_gl_realize(GtkGLArea* area, gpointer user_data);
static void on_gl_unrealize(GtkGLArea* area, gpointer user_data);
static void on_gl_resize(GtkGLArea* area, gint width, gint height, gpointer user_data);
static void send_event(MpvPlugin* self, FlValue* event) {
if (self->event_channel) {
g_autoptr(GError) error = nullptr;
if (!fl_event_channel_send(self->event_channel, event, nullptr, &error)) {
if (error != nullptr) {
g_warning("Failed to send event: %s", error->message);
}
}
}
}
static void mpv_plugin_dispose(GObject* object) {
MpvPlugin* self = MPV_PLUGIN(object);
@@ -41,6 +43,16 @@ static void mpv_plugin_dispose(GObject* object) {
self->player.reset();
}
if (self->texture) {
mpv_texture_dispose(self->texture);
if (self->texture_registrar) {
fl_texture_registrar_unregister_texture(self->texture_registrar,
FL_TEXTURE(self->texture));
}
g_object_unref(self->texture);
self->texture = nullptr;
}
g_clear_object(&self->method_channel);
g_clear_object(&self->event_channel);
g_clear_object(&self->registrar);
@@ -55,33 +67,18 @@ static void mpv_plugin_class_init(MpvPluginClass* klass) {
static void mpv_plugin_init(MpvPlugin* self) {
self->visible = FALSE;
self->initialized = FALSE;
self->texture = nullptr;
self->texture_registrar = nullptr;
}
/// Send an event through the event channel.
static void send_event(MpvPlugin* self, FlValue* event) {
if (self->event_channel) {
g_autoptr(GError) error = nullptr;
if (!fl_event_channel_send(self->event_channel, event, nullptr, &error)) {
if (error != nullptr) {
g_warning("Failed to send event: %s", error->message);
}
}
}
}
MpvPlugin* mpv_plugin_new(FlPluginRegistrar* registrar,
GtkOverlay* overlay,
GtkGLArea* gl_area,
GtkWidget* flutter_view) {
MpvPlugin* mpv_plugin_new(FlPluginRegistrar* registrar) {
MpvPlugin* self = MPV_PLUGIN(g_object_new(MPV_PLUGIN_TYPE, nullptr));
self->registrar = FL_PLUGIN_REGISTRAR(g_object_ref(registrar));
self->overlay = overlay;
self->gl_area = gl_area;
self->flutter_view = flutter_view;
self->texture_registrar =
fl_plugin_registrar_get_texture_registrar(registrar);
self->player = std::make_unique<mpv::MpvPlayer>();
// Create method channel.
g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new();
self->method_channel = fl_method_channel_new(
fl_plugin_registrar_get_messenger(registrar),
@@ -94,167 +91,19 @@ MpvPlugin* mpv_plugin_new(FlPluginRegistrar* registrar,
self,
nullptr);
// Create event channel.
self->event_channel = fl_event_channel_new(
fl_plugin_registrar_get_messenger(registrar),
"com.plezy/mpv_player/events",
FL_METHOD_CODEC(codec));
// Connect GtkGLArea signals.
g_signal_connect(gl_area, "render", G_CALLBACK(on_gl_render), self);
g_signal_connect(gl_area, "realize", G_CALLBACK(on_gl_realize), self);
g_signal_connect(gl_area, "unrealize", G_CALLBACK(on_gl_unrealize), self);
g_signal_connect(gl_area, "resize", G_CALLBACK(on_gl_resize), self);
// Set up auto-render to false - we control when to render.
gtk_gl_area_set_auto_render(gl_area, FALSE);
// Use OpenGL 3.3 core profile.
gtk_gl_area_set_required_version(gl_area, 3, 3);
return self;
}
// Static reference to keep the plugin alive for the lifetime of the app.
// The plugin will be disposed when the GL area is unrealized.
// Static reference to keep the plugin alive.
static MpvPlugin* g_mpv_plugin = nullptr;
void mpv_plugin_register_with_registrar(FlPluginRegistrar* registrar,
GtkOverlay* overlay,
GtkGLArea* gl_area,
GtkWidget* flutter_view) {
g_mpv_plugin = mpv_plugin_new(registrar, overlay, gl_area, flutter_view);
// Keep a reference - the plugin will be cleaned up when the app exits
}
/// GtkGLArea render callback.
static gboolean on_gl_render(GtkGLArea* area,
GdkGLContext* context,
gpointer user_data) {
(void)context;
MpvPlugin* self = MPV_PLUGIN(user_data);
// Ensure GL context is current before any GL operations.
// Critical during fullscreen transitions and workspace switches (issue #202).
gtk_gl_area_make_current(area);
// Check for GL context errors (can happen during window state changes)
GError* error = gtk_gl_area_get_error(area);
if (error != nullptr) {
g_warning("MPV Plugin: GL context error in render: %s", error->message);
return FALSE; // Signal failure to GTK
}
if (!self->player || !self->player->IsInitialized() || !self->visible) {
// Clear to transparent when not showing video.
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
glClear(GL_COLOR_BUFFER_BIT);
return TRUE;
}
int width = gtk_widget_get_allocated_width(GTK_WIDGET(area));
int height = gtk_widget_get_allocated_height(GTK_WIDGET(area));
// Get the scale factor for HiDPI support.
int scale = gtk_widget_get_scale_factor(GTK_WIDGET(area));
width *= scale;
height *= scale;
// Get the FBO that GtkGLArea is rendering to.
// GtkGLArea uses its own FBO, not the default framebuffer (0).
GLint fbo = 0;
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &fbo);
// Save GL state before MPV render (MPV modifies these and doesn't restore them)
// This prevents GL state pollution that corrupts Flutter's rendering.
GLint prev_viewport[4];
GLint prev_scissor_box[4];
GLboolean prev_blend, prev_scissor_test;
GLint prev_blend_src, prev_blend_dst;
glGetIntegerv(GL_VIEWPORT, prev_viewport);
glGetIntegerv(GL_SCISSOR_BOX, prev_scissor_box);
glGetBooleanv(GL_BLEND, &prev_blend);
glGetBooleanv(GL_SCISSOR_TEST, &prev_scissor_test);
glGetIntegerv(GL_BLEND_SRC_ALPHA, &prev_blend_src);
glGetIntegerv(GL_BLEND_DST_ALPHA, &prev_blend_dst);
// Set viewport and render the video frame.
glViewport(0, 0, width, height);
self->player->ClearRedrawFlag();
self->player->Render(width, height, fbo);
// Restore GL state after MPV render to prevent Flutter corruption.
glViewport(prev_viewport[0], prev_viewport[1], prev_viewport[2], prev_viewport[3]);
glScissor(prev_scissor_box[0], prev_scissor_box[1], prev_scissor_box[2], prev_scissor_box[3]);
if (prev_blend) {
glEnable(GL_BLEND);
} else {
glDisable(GL_BLEND);
}
if (prev_scissor_test) {
glEnable(GL_SCISSOR_TEST);
} else {
glDisable(GL_SCISSOR_TEST);
}
glBlendFunc(prev_blend_src, prev_blend_dst);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
return TRUE;
}
/// GtkGLArea realize callback.
static void on_gl_realize(GtkGLArea* area, gpointer user_data) {
(void)user_data;
gtk_gl_area_make_current(area);
// Check for GL errors.
GError* error = gtk_gl_area_get_error(area);
if (error != nullptr) {
g_warning("MPV Plugin: GL area error: %s", error->message);
return;
}
// Enable blending for transparency support.
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
g_message("MPV Plugin: GL area realized");
}
/// GtkGLArea unrealize callback.
static void on_gl_unrealize(GtkGLArea* area, gpointer user_data) {
MpvPlugin* self = MPV_PLUGIN(user_data);
gtk_gl_area_make_current(area);
// Check if context is valid before disposing GL resources (issue #202)
GError* error = gtk_gl_area_get_error(area);
if (error != nullptr) {
g_warning("MPV Plugin: GL context error in unrealize: %s", error->message);
}
// Always try to dispose - Dispose() handles its own safety checks
if (self->player) {
self->player->Dispose();
}
g_message("MPV Plugin: GL area unrealized");
}
/// GtkGLArea resize callback.
static void on_gl_resize(GtkGLArea* area,
gint width,
gint height,
gpointer user_data) {
MpvPlugin* self = MPV_PLUGIN(user_data);
(void)width;
(void)height;
// Force a redraw when size changes to prevent lag during resize.
if (self->visible && self->player && self->player->IsInitialized()) {
gtk_gl_area_queue_render(area);
}
void mpv_plugin_register_with_registrar(FlPluginRegistrar* registrar) {
g_mpv_plugin = mpv_plugin_new(registrar);
}
/// Method call handler.
@@ -269,40 +118,41 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel,
g_autoptr(FlMethodResponse) response = nullptr;
if (strcmp(method, "initialize") == 0) {
if (self->initialized) {
response = FL_METHOD_RESPONSE(
fl_method_success_response_new(fl_value_new_bool(TRUE)));
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))));
} else {
// Create player if it was disposed
if (!self->player) {
self->player = std::make_unique<mpv::MpvPlayer>();
}
// Check if GL area is realized before trying to use it
if (!gtk_widget_get_realized(GTK_WIDGET(self->gl_area))) {
// Force realization of the GL area
gtk_widget_realize(GTK_WIDGET(self->gl_area));
}
if (self->player->Initialize()) {
// Create the FlTextureGL and register it
FlView* view = fl_plugin_registrar_get_view(self->registrar);
self->texture = mpv_texture_new(
self->player.get(), self->texture_registrar, view);
// Initialize the player with the GL area.
gtk_gl_area_make_current(self->gl_area);
fl_texture_registrar_register_texture(
self->texture_registrar, FL_TEXTURE(self->texture));
// Set redraw callback: when mpv has a frame, mark texture available
MpvTexture* tex = self->texture;
self->player->SetRedrawCallback([tex]() {
mpv_texture_mark_frame_available(tex);
});
GError* error = gtk_gl_area_get_error(self->gl_area);
if (error != nullptr) {
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
"GL_ERROR", error->message, nullptr));
} else if (self->player->Initialize(self->gl_area)) {
self->initialized = TRUE;
// Set up event callback.
// Set up event callback
self->player->SetEventCallback([self](FlValue* event) {
// Send event - must be called from main thread
// The event is already created on the main thread via g_idle_add in mpv_player.cc
send_event(self, event);
});
response = FL_METHOD_RESPONSE(
fl_method_success_response_new(fl_value_new_bool(TRUE)));
// Return the texture ID for the Dart Texture widget
response = FL_METHOD_RESPONSE(fl_method_success_response_new(
fl_value_new_int(mpv_texture_get_id(self->texture))));
} else {
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
"INIT_FAILED", "Failed to initialize MPV player", nullptr));
@@ -310,18 +160,18 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel,
}
} else if (strcmp(method, "dispose") == 0) {
if (self->player) {
// Make GL context current before disposing mpv GL resources
gtk_gl_area_make_current(self->gl_area);
self->player->Dispose();
self->player.reset();
}
if (self->texture) {
mpv_texture_dispose(self->texture);
fl_texture_registrar_unregister_texture(self->texture_registrar,
FL_TEXTURE(self->texture));
g_object_unref(self->texture);
self->texture = nullptr;
}
self->initialized = FALSE;
self->visible = FALSE;
gtk_widget_set_visible(GTK_WIDGET(self->gl_area), FALSE);
if (self->flutter_view != nullptr) {
// Force Flutter view to redraw
gtk_widget_queue_draw(self->flutter_view);
}
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
} else if (strcmp(method, "command") == 0) {
if (!self->player || !self->initialized) {
@@ -342,8 +192,6 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel,
command_args.push_back(fl_value_get_string(item));
}
}
// Use async command to prevent UI blocking during network operations
// Take ownership of method_call to respond asynchronously
g_object_ref(method_call);
self->player->CommandAsync(command_args, [method_call](int error) {
g_autoptr(FlMethodResponse) async_response = nullptr;
@@ -356,7 +204,7 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel,
fl_method_call_respond(method_call, async_response, nullptr);
g_object_unref(method_call);
});
return; // Response will be sent asynchronously
return; // Response sent asynchronously
}
}
} else if (strcmp(method, "setProperty") == 0) {
@@ -456,14 +304,11 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel,
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
"INVALID_ARGS", "Missing 'visible'", nullptr));
} else {
gboolean visible = fl_value_get_bool(visible_value);
self->visible = visible;
self->visible = fl_value_get_bool(visible_value);
// Show/hide the GL area.
gtk_widget_set_visible(GTK_WIDGET(self->gl_area), visible);
if (visible) {
gtk_gl_area_queue_render(self->gl_area);
if (self->visible && self->texture) {
// Trigger a frame render when becoming visible
mpv_texture_mark_frame_available(self->texture);
}
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
+4 -15
View File
@@ -2,7 +2,6 @@
#define MPV_PLUGIN_H_
#include <flutter_linux/flutter_linux.h>
#include <gtk/gtk.h>
#include <memory>
@@ -12,28 +11,18 @@ G_BEGIN_DECLS
/// Plugin for MPV video playback on Linux.
///
/// This plugin uses OpenGL rendering via GtkGLArea,
/// positioned behind the Flutter view using a GtkOverlay.
/// This plugin renders mpv video through Flutter's GPU-accelerated
/// texture pipeline via FlTextureGL.
#define MPV_PLUGIN_TYPE (mpv_plugin_get_type())
G_DECLARE_FINAL_TYPE(MpvPlugin, mpv_plugin, MPV, PLUGIN, GObject)
/// Creates a new MpvPlugin instance.
/// @param registrar The Flutter plugin registrar.
/// @param overlay The GtkOverlay containing the GtkGLArea and FlView.
/// @param gl_area The GtkGLArea widget for video rendering.
/// @param flutter_view The Flutter view widget (for visibility control).
MpvPlugin* mpv_plugin_new(FlPluginRegistrar* registrar,
GtkOverlay* overlay,
GtkGLArea* gl_area,
GtkWidget* flutter_view);
MpvPlugin* mpv_plugin_new(FlPluginRegistrar* registrar);
/// Registers the plugin with Flutter.
void mpv_plugin_register_with_registrar(FlPluginRegistrar* registrar,
GtkOverlay* overlay,
GtkGLArea* gl_area,
GtkWidget* flutter_view);
void mpv_plugin_register_with_registrar(FlPluginRegistrar* registrar);
G_END_DECLS
+182
View File
@@ -0,0 +1,182 @@
#include "mpv_texture.h"
#include <epoxy/gl.h>
struct _MpvTexture {
FlTextureGL parent_instance;
mpv::MpvPlayer* player; // not owned
FlTextureRegistrar* registrar; // not owned
FlView* view; // not owned, for querying allocation size
GLuint fbo;
GLuint texture;
int32_t width;
int32_t height;
};
G_DEFINE_TYPE(MpvTexture, mpv_texture, fl_texture_gl_get_type())
static void ensure_fbo(MpvTexture* self, int32_t w, int32_t h) {
if (self->fbo != 0 && self->width == w && self->height == h) {
return;
}
// Delete old resources.
if (self->fbo != 0) {
glDeleteFramebuffers(1, &self->fbo);
self->fbo = 0;
}
if (self->texture != 0) {
glDeleteTextures(1, &self->texture);
self->texture = 0;
}
self->width = w;
self->height = h;
glGenTextures(1, &self->texture);
glBindTexture(GL_TEXTURE_2D, self->texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA,
GL_UNSIGNED_BYTE, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glGenFramebuffers(1, &self->fbo);
glBindFramebuffer(GL_FRAMEBUFFER, self->fbo);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
self->texture, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
static gboolean mpv_texture_populate(FlTextureGL* gl_texture,
uint32_t* target,
uint32_t* name,
uint32_t* width,
uint32_t* height,
GError** error) {
MpvTexture* self = MPV_TEXTURE(gl_texture);
if (!self->player) {
return FALSE;
}
// Lazily create the mpv render context on first populate() call,
// since Flutter's GL context is current here.
if (!self->player->HasRenderContext()) {
if (!self->player->InitRenderContext()) {
g_set_error(error, g_quark_from_static_string("mpv"), 0,
"Failed to create mpv render context");
return FALSE;
}
}
// Determine target size from the FlView widget allocation.
GtkAllocation alloc;
gtk_widget_get_allocation(GTK_WIDGET(self->view), &alloc);
int scale = gtk_widget_get_scale_factor(GTK_WIDGET(self->view));
int32_t w = alloc.width * scale;
int32_t h = alloc.height * scale;
if (w <= 0 || h <= 0) {
return FALSE;
}
ensure_fbo(self, w, h);
// Save GL state that mpv may clobber.
GLint prev_viewport[4];
GLint prev_scissor_box[4];
GLboolean prev_blend, prev_scissor_test;
GLint prev_blend_src, prev_blend_dst;
GLint prev_fbo;
glGetIntegerv(GL_VIEWPORT, prev_viewport);
glGetIntegerv(GL_SCISSOR_BOX, prev_scissor_box);
glGetBooleanv(GL_BLEND, &prev_blend);
glGetBooleanv(GL_SCISSOR_TEST, &prev_scissor_test);
glGetIntegerv(GL_BLEND_SRC_ALPHA, &prev_blend_src);
glGetIntegerv(GL_BLEND_DST_ALPHA, &prev_blend_dst);
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &prev_fbo);
// Render mpv into our FBO.
glBindFramebuffer(GL_FRAMEBUFFER, self->fbo);
glViewport(0, 0, w, h);
self->player->ClearRedrawFlag();
self->player->Render(w, h, static_cast<int>(self->fbo));
// Restore GL state.
glViewport(prev_viewport[0], prev_viewport[1], prev_viewport[2],
prev_viewport[3]);
glScissor(prev_scissor_box[0], prev_scissor_box[1], prev_scissor_box[2],
prev_scissor_box[3]);
if (prev_blend)
glEnable(GL_BLEND);
else
glDisable(GL_BLEND);
if (prev_scissor_test)
glEnable(GL_SCISSOR_TEST);
else
glDisable(GL_SCISSOR_TEST);
glBlendFunc(prev_blend_src, prev_blend_dst);
glBindFramebuffer(GL_FRAMEBUFFER, prev_fbo);
*target = GL_TEXTURE_2D;
*name = self->texture;
*width = static_cast<uint32_t>(w);
*height = static_cast<uint32_t>(h);
return TRUE;
}
static void mpv_texture_class_init(MpvTextureClass* klass) {
FL_TEXTURE_GL_CLASS(klass)->populate = mpv_texture_populate;
}
static void mpv_texture_init(MpvTexture* self) {
self->player = nullptr;
self->registrar = nullptr;
self->view = nullptr;
self->fbo = 0;
self->texture = 0;
self->width = 0;
self->height = 0;
}
MpvTexture* mpv_texture_new(mpv::MpvPlayer* player,
FlTextureRegistrar* registrar,
FlView* view) {
MpvTexture* self = MPV_TEXTURE(g_object_new(MPV_TEXTURE_TYPE, nullptr));
self->player = player;
self->registrar = registrar;
self->view = view;
return self;
}
void mpv_texture_mark_frame_available(MpvTexture* self) {
if (self && self->registrar) {
fl_texture_registrar_mark_texture_frame_available(
self->registrar, FL_TEXTURE(self));
}
}
void mpv_texture_dispose(MpvTexture* self) {
if (!self) return;
if (self->fbo != 0) {
glDeleteFramebuffers(1, &self->fbo);
self->fbo = 0;
}
if (self->texture != 0) {
glDeleteTextures(1, &self->texture);
self->texture = 0;
}
self->player = nullptr;
self->registrar = nullptr;
self->view = nullptr;
}
int64_t mpv_texture_get_id(MpvTexture* self) {
return fl_texture_get_id(FL_TEXTURE(self));
}
+30
View File
@@ -0,0 +1,30 @@
#ifndef MPV_TEXTURE_H_
#define MPV_TEXTURE_H_
#include <flutter_linux/flutter_linux.h>
#include "mpv_player.h"
G_BEGIN_DECLS
#define MPV_TEXTURE_TYPE (mpv_texture_get_type())
G_DECLARE_FINAL_TYPE(MpvTexture, mpv_texture, MPV, TEXTURE, FlTextureGL)
/// Creates a new MpvTexture that renders mpv video to an offscreen FBO.
MpvTexture* mpv_texture_new(mpv::MpvPlayer* player,
FlTextureRegistrar* registrar,
FlView* view);
/// Notifies Flutter that a new frame is available.
void mpv_texture_mark_frame_available(MpvTexture* self);
/// Cleans up GL resources (FBO/texture).
void mpv_texture_dispose(MpvTexture* self);
/// Returns the Flutter texture ID.
int64_t mpv_texture_get_id(MpvTexture* self);
G_END_DECLS
#endif // MPV_TEXTURE_H_
+4 -85
View File
@@ -11,29 +11,11 @@
struct _MyApplication {
GtkApplication parent_instance;
char** dart_entrypoint_arguments;
// MPV-related widgets
GtkOverlay* overlay;
GtkGLArea* gl_area;
FlView* flutter_view;
};
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
/// Sets up an RGBA visual for transparency support.
static void setup_rgba_visual(GtkWidget* widget) {
GdkScreen* screen = gtk_widget_get_screen(widget);
if (!gdk_screen_is_composited(screen)) {
g_warning("Screen is not composited - transparency may not work");
}
GdkVisual* visual = gdk_screen_get_rgba_visual(screen);
if (visual != nullptr) {
gtk_widget_set_visual(widget, visual);
} else {
g_warning("No RGBA visual available");
}
}
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
@@ -69,72 +51,23 @@ static void my_application_activate(GApplication* application) {
gtk_window_set_default_size(window, 1280, 720);
// Set up RGBA visual for transparency support.
gtk_widget_set_app_paintable(GTK_WIDGET(window), TRUE);
setup_rgba_visual(GTK_WIDGET(window));
// Create the overlay container.
// The overlay allows us to layer widgets on top of each other:
// - Bottom layer: GtkGLArea for mpv video rendering
// - Top layer: FlView (Flutter) with transparent background
self->overlay = GTK_OVERLAY(gtk_overlay_new());
gtk_widget_show(GTK_WIDGET(self->overlay));
// Create the GtkGLArea for mpv video rendering.
// This will be the bottom layer (behind Flutter).
self->gl_area = GTK_GL_AREA(gtk_gl_area_new());
gtk_widget_set_hexpand(GTK_WIDGET(self->gl_area), TRUE);
gtk_widget_set_vexpand(GTK_WIDGET(self->gl_area), TRUE);
// Configure GL area for transparency and proper rendering.
gtk_gl_area_set_has_alpha(self->gl_area, TRUE);
gtk_gl_area_set_has_depth_buffer(self->gl_area, FALSE);
gtk_gl_area_set_has_stencil_buffer(self->gl_area, FALSE);
// Make GL area non-interactive so mouse events pass through to Flutter.
gtk_widget_set_can_focus(GTK_WIDGET(self->gl_area), FALSE);
gtk_widget_set_sensitive(GTK_WIDGET(self->gl_area), FALSE);
// Set the GL area as the base widget of the overlay.
// Initially hidden - will be shown when video playback starts.
gtk_widget_set_visible(GTK_WIDGET(self->gl_area), FALSE);
gtk_container_add(GTK_CONTAINER(self->overlay), GTK_WIDGET(self->gl_area));
// Create the Flutter view.
// Create the Flutter view (opaque — no overlay needed).
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(project,
self->dart_entrypoint_arguments);
self->flutter_view = fl_view_new(project);
gtk_widget_set_hexpand(GTK_WIDGET(self->flutter_view), TRUE);
gtk_widget_set_vexpand(GTK_WIDGET(self->flutter_view), TRUE);
// Set up RGBA visual for transparency support.
// Note: app_paintable is only set on the window (line 73), not on child widgets,
// to avoid redundant composition passes.
setup_rgba_visual(GTK_WIDGET(self->flutter_view));
// Enable transparent background for the Flutter view.
// This allows the mpv video to show through transparent areas.
GdkRGBA transparent = {0.0, 0.0, 0.0, 0.0};
fl_view_set_background_color(self->flutter_view, &transparent);
// Add the Flutter view as an overlay on top of the GL area.
gtk_widget_show(GTK_WIDGET(self->flutter_view));
gtk_overlay_add_overlay(self->overlay, GTK_WIDGET(self->flutter_view));
// Add the overlay to the window.
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(self->overlay));
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(self->flutter_view));
// Register Flutter plugins.
fl_register_plugins(FL_PLUGIN_REGISTRY(self->flutter_view));
// Register the MPV plugin with the GL area and Flutter view for video rendering.
// Register the MPV plugin (uses FlTextureGL — no overlay/GtkGLArea needed).
FlPluginRegistrar* registrar =
fl_plugin_registry_get_registrar_for_plugin(FL_PLUGIN_REGISTRY(self->flutter_view),
"MpvPlugin");
mpv_plugin_register_with_registrar(registrar, self->overlay, self->gl_area,
GTK_WIDGET(self->flutter_view));
mpv_plugin_register_with_registrar(registrar);
gtk_widget_show(GTK_WIDGET(window));
gtk_widget_grab_focus(GTK_WIDGET(self->flutter_view));
@@ -163,19 +96,11 @@ static gboolean my_application_local_command_line(GApplication* application,
// Implements GApplication::startup.
static void my_application_startup(GApplication* application) {
// MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application startup.
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
}
// Implements GApplication::shutdown.
static void my_application_shutdown(GApplication* application) {
// MyApplication* self = MY_APPLICATION(object);
// Perform any actions required at application shutdown.
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
}
@@ -196,16 +121,10 @@ static void my_application_class_init(MyApplicationClass* klass) {
}
static void my_application_init(MyApplication* self) {
self->overlay = nullptr;
self->gl_area = nullptr;
self->flutter_view = nullptr;
}
MyApplication* my_application_new() {
// Set the program name to the application ID, which helps various systems
// like GTK and desktop environments map this running application to its
// corresponding .desktop file. This ensures better integration by allowing
// the application to be recognized beyond its binary name.
g_set_prgname(APPLICATION_ID);
return MY_APPLICATION(g_object_new(my_application_get_type(),