Vendor media_kit_video 2.0.1 with the X11/EGL fix (hardware rendering on X11 with Flutter >= 3.38)
ci / core (push) Successful in 28s
ci / server (push) Successful in 51s
ci / updater-windows (push) Failing after 5m59s

Flutter >= 3.38 keeps its EGL contexts on the raster thread, so
media_kit's video output found none on the platform thread and fell
back to software rendering (no shaders, ~5 fps) on X11. The patched
output opens Flutter's EGL display itself, steps GDK's GLX context aside
on X11 and releases its own context. Patch from a friend's agent, kept
in third_party/media_kit_video/starsync/. Wayland/NVIDIA still renders
on the GPU (checked); the X11 path was verified on the friend's machine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-16 01:13:14 +03:00
1 parent 25b19f2d6a
commit a87ee98faa
131 files changed
+15532 -6

No files matched your search

+99
View File
@@ -0,0 +1,99 @@
# This file is a part of media_kit (https://github.com/media-kit/media-kit).
#
# Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
# All rights reserved.
# Use of this source code is governed by MIT license that can be found in the LICENSE file.
cmake_minimum_required(VERSION 3.10)
set(PROJECT_NAME "media_kit_video")
set(CMAKE_CXX_STANDARD 17)
project(${PROJECT_NAME} LANGUAGES CXX)
set(PLUGIN_NAME "media_kit_video_plugin")
if(MEDIA_KIT_LIBS_AVAILABLE)
add_library(
${PLUGIN_NAME} SHARED
"media_kit_video_plugin.cc"
"texture_gl.cc"
"texture_sw.cc"
"video_output_manager.cc"
"video_output.cc"
"utils.cc"
)
apply_standard_settings(${PLUGIN_NAME})
# Check for libmpv & epoxy headers & libraries.
find_package(PkgConfig REQUIRED)
pkg_check_modules(mpv IMPORTED_TARGET mpv)
pkg_check_modules(epoxy IMPORTED_TARGET epoxy)
set_target_properties(
${PLUGIN_NAME} PROPERTIES
CXX_VISIBILITY_PRESET hidden
)
target_compile_definitions(
${PLUGIN_NAME} PRIVATE
FLUTTER_PLUGIN_IMPL
)
target_compile_options(${PLUGIN_NAME} PRIVATE "${mpv_CFLAGS_OTHER}")
target_compile_options(${PLUGIN_NAME} PRIVATE "${epoxy_CFLAGS_OTHER}")
target_include_directories(
${PLUGIN_NAME} INTERFACE
"${CMAKE_CURRENT_SOURCE_DIR}/include"
"${mpv_INCLUDE_DIRS}"
"${epoxy_INCLUDE_DIRS}"
)
target_link_libraries(
${PLUGIN_NAME} PRIVATE
flutter
PkgConfig::GTK
PkgConfig::mpv
PkgConfig::epoxy
)
else()
message(NOTICE "media_kit: WARNING: package:media_kit_libs_*** not found.")
add_library(
${PLUGIN_NAME} SHARED
"media_kit_video_plugin.cc"
)
apply_standard_settings(${PLUGIN_NAME})
set_target_properties(
${PLUGIN_NAME} PROPERTIES
CXX_VISIBILITY_PRESET hidden
)
target_compile_definitions(
${PLUGIN_NAME} PRIVATE
FLUTTER_PLUGIN_IMPL
# Add macro to indicate that package:media_kit_libs_*** is not available.
"MEDIA_KIT_LIBS_NOT_FOUND=1"
)
target_include_directories(
${PLUGIN_NAME} INTERFACE
"${CMAKE_CURRENT_SOURCE_DIR}/include"
)
target_link_libraries(
${PLUGIN_NAME} PRIVATE
flutter
PkgConfig::GTK
)
endif()
set(
media_kit_video_bundled_libraries
""
PARENT_SCOPE
)
@@ -0,0 +1,34 @@
// This file is a part of media_kit
// (https://github.com/media-kit/media-kit).
//
// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
// All rights reserved.
// Use of this source code is governed by MIT license that can be found in the
// LICENSE file.
#ifndef MEDIA_KIT_VIDEO_PLUGIN_H_
#define MEDIA_KIT_VIDEO_PLUGIN_H_
#include <flutter_linux/flutter_linux.h>
G_BEGIN_DECLS
#ifdef FLUTTER_PLUGIN_IMPL
#define FLUTTER_PLUGIN_EXPORT __attribute__((visibility("default")))
#else
#define FLUTTER_PLUGIN_EXPORT
#endif
typedef struct _MediaKitVideoPlugin MediaKitVideoPlugin;
typedef struct {
GObjectClass parent_class;
} MediaKitVideoPluginClass;
FLUTTER_PLUGIN_EXPORT GType media_kit_video_plugin_get_type();
FLUTTER_PLUGIN_EXPORT void media_kit_video_plugin_register_with_registrar(
FlPluginRegistrar* registrar);
G_END_DECLS
#endif // FLUTTER_PLUGIN_MEDIA_KIT_VIDEO_PLUGIN_H_
@@ -0,0 +1,35 @@
// This file is a part of media_kit
// (https://github.com/media-kit/media-kit).
//
// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
// All rights reserved.
// Use of this source code is governed by MIT license that can be found in the
// LICENSE file.
#ifndef TEXTURE_GL_H_
#define TEXTURE_GL_H_
#include <flutter_linux/flutter_linux.h>
#include "video_output.h"
#define TEXTURE_GL_TYPE (texture_gl_get_type())
G_DECLARE_FINAL_TYPE(TextureGL, texture_gl, TEXTURE_GL, TEXTURE_GL, FlTextureGL)
#define TEXTURE_GL(obj) \
(G_TYPE_CHECK_INSTANCE_CAST((obj), texture_gl_get_type(), TextureGL))
TextureGL* texture_gl_new(VideoOutput* video_output);
/**
* @brief Populates texture with video frame.
*/
gboolean texture_gl_populate_texture(FlTextureGL* texture,
guint32* target,
guint32* name,
guint32* width,
guint32* height,
GError** error);
#endif // TEXTURE_GL_H_
@@ -0,0 +1,42 @@
// This file is a part of media_kit
// (https://github.com/media-kit/media-kit).
//
// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
// All rights reserved.
// Use of this source code is governed by MIT license that can be found in the
// LICENSE file.
#ifndef TEXTURE_SW_H_
#define TEXTURE_SW_H_
#include <flutter_linux/flutter_linux.h>
#include "video_output.h"
#define TEXTURE_SW_TYPE (texture_sw_get_type())
G_DECLARE_FINAL_TYPE(TextureSW,
texture_sw,
TEXTURE_SW,
TEXTURE_SW,
FlPixelBufferTexture)
#define TEXTURE_SW(obj) \
(G_TYPE_CHECK_INSTANCE_CAST((obj), texture_sw_get_type(), TextureSW))
TextureSW* texture_sw_new(VideoOutput* video_output);
gboolean texture_sw_copy_pixels(FlPixelBufferTexture* texture,
const guint8** buffer,
guint32* width,
guint32* height,
GError** error);
// Limit the frame size to 1080p in software rendering.
// This is for performance reasons & to avoid allocating too much memory.
#define SW_RENDERING_MAX_WIDTH 1920
#define SW_RENDERING_MAX_HEIGHT 1080
#define SW_RENDERING_PIXEL_BUFFER_SIZE \
(SW_RENDERING_MAX_WIDTH) * (SW_RENDERING_MAX_HEIGHT) * (4)
#endif // TEXTURE_SW_H_
@@ -0,0 +1,13 @@
// This file is a part of media_kit
// (https://github.com/media-kit/media-kit).
//
// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
// All rights reserved.
// Use of this source code is governed by MIT license that can be found in the
// LICENSE file.
#include <gtk/gtk.h>
void utils_enter_native_fullscreen(GtkWidget* window);
void utils_exit_native_fullscreen(GtkWidget* window);
@@ -0,0 +1,133 @@
// This file is a part of media_kit
// (https://github.com/media-kit/media-kit).
//
// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
// All rights reserved.
// Use of this source code is governed by MIT license that can be found in the
// LICENSE file.
#ifndef VIDEO_OUTPUT_H_
#define VIDEO_OUTPUT_H_
#include <flutter_linux/flutter_linux.h>
#include <epoxy/egl.h>
#include <epoxy/glx.h>
#include "mpv/client.h"
#include "mpv/render.h"
#include "mpv/render_gl.h"
typedef struct _VideoOutputConfiguration {
gint64 width;
gint64 height;
bool enable_hardware_acceleration;
_VideoOutputConfiguration(gint64 width = NULL,
gint64 height = NULL,
bool enable_hardware_acceleration = true)
: width(width),
height(height),
enable_hardware_acceleration(enable_hardware_acceleration) {}
} VideoOutputConfiguration;
// Callback invoked when the texture ID updates i.e. video dimensions changes.
typedef void (*TextureUpdateCallback)(gint64 id,
gint64 width,
gint64 height,
gpointer context);
#define VIDEO_OUTPUT_TYPE (video_output_get_type())
G_DECLARE_FINAL_TYPE(VideoOutput,
video_output,
VIDEO_OUTPUT,
VIDEO_OUTPUT,
GObject)
#define VIDEO_OUTPUT(obj) \
(G_TYPE_CHECK_INSTANCE_CAST((obj), video_output_get_type(), VideoOutput))
/**
* @brief Creates a new |VideoOutput| instance for given |handle|.
*
* @param texture_registrar |FlTextureRegistrar| reference.
* @param handle |mpv_handle| reference casted to gint64.
* @param width Preferred width of the video. Pass `NULL` for using texture
* dimensions based on video's resolution.
* @param height Preferred height of the video. Pass `NULL` for using texture
* dimensions based on video's resolution.
* @param enable_hardware_acceleration Whether to enable hardware acceleration.
* @return VideoOutput*
*/
VideoOutput* video_output_new(FlTextureRegistrar* texture_registrar,
FlView* view,
gint64 handle,
VideoOutputConfiguration configuration);
/**
* @brief Sets the callback invoked when the texture ID updates i.e. video
* dimensions changes.
*
* @param self |VideoOutput| reference.
* @param texture_update_callback Callback.
* @param texture_update_callback_context Callback context.
*/
void video_output_set_texture_update_callback(
VideoOutput* self,
TextureUpdateCallback texture_update_callback,
gpointer texture_update_callback_context);
/**
* @brief Sets the required video output size. This forces |VideoOutput| to
* resize the internal OpenGL surface / texture.
*
* @param texture_registrar |FlTextureRegistrar| reference.
* @param width Preferred width of the video. Pass `NULL` for using texture
* dimensions based on video's resolution.
* @param height Preferred height of the video. Pass `NULL` for using texture
* dimensions based on video's resolution.
*/
void video_output_set_size(VideoOutput* self, gint64 width, gint64 height);
mpv_render_context* video_output_get_render_context(VideoOutput* self);
GdkGLContext* video_output_get_gdk_gl_context(VideoOutput* self);
EGLDisplay video_output_get_egl_display(VideoOutput* self);
// GDK's GLX context of the calling thread, set aside while an EGL context
// is used there.
typedef struct {
Display* display;
GLXDrawable draw;
GLXDrawable read;
GLXContext context;
} VideoOutputGlxState;
void video_output_glx_release(VideoOutputGlxState* state);
void video_output_glx_restore(VideoOutputGlxState* state);
// Makes |context| current again, or, when it is EGL_NO_CONTEXT, releases
// whatever context of |own_display| this thread holds.
void video_output_restore_egl_context(EGLDisplay own_display,
EGLDisplay display,
EGLSurface draw,
EGLSurface read,
EGLContext context);
EGLContext video_output_get_egl_context(VideoOutput* self);
EGLSurface video_output_get_egl_surface(VideoOutput* self);
guint8* video_output_get_pixel_buffer(VideoOutput* self);
gint64 video_output_get_width(VideoOutput* self);
gint64 video_output_get_height(VideoOutput* self);
gint64 video_output_get_texture_id(VideoOutput* self);
void video_output_notify_texture_update(VideoOutput* self);
#endif // VIDEO_OUTPUT_H_
@@ -0,0 +1,75 @@
// This file is a part of media_kit
// (https://github.com/media-kit/media-kit).
//
// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
// All rights reserved.
// Use of this source code is governed by MIT license that can be found in the
// LICENSE file.
#ifndef VIDEO_OUTPUT_MANAGER_H_
#define VIDEO_OUTPUT_MANAGER_H_
#include "video_output.h"
#define VIDEO_OUTPUT_MANAGER_TYPE (video_output_manager_get_type())
// Creates & disposes |VideoOutput| instances for video embedding.
G_DECLARE_FINAL_TYPE(VideoOutputManager,
video_output_manager,
VIDEO_OUTPUT_MANAGER,
VIDEO_OUTPUT_MANAGER,
GObject)
#define VIDEO_OUTPUT_MANAGER(obj) \
(G_TYPE_CHECK_INSTANCE_CAST((obj), video_output_manager_get_type(), \
VideoOutputManager))
VideoOutputManager* video_output_manager_new(
FlTextureRegistrar* texture_registrar,
FlView* view);
/**
* @brief Creates a new |VideoOutput| instance for given |handle|.
*
* @param self |VideoOutputManager| reference.
* @param handle |mpv_handle| reference casted to gint64.
* @param width Preferred width of the video. Pass `NULL` for using texture
* dimensions based on video's resolution.
* @param height Preferred height of the video. Pass `NULL` for using texture
* dimensions based on video's resolution.
* @param enable_hardware_acceleration Whether to enable hardware acceleration.
* @param texture_update_callback Callback invoked when the texture ID updates
* i.e. video dimensions changes.
* @param texture_update_callback_context Context passed to
* |texture_update_callback|.
*/
void video_output_manager_create(VideoOutputManager* self,
gint64 handle,
VideoOutputConfiguration configuration,
TextureUpdateCallback texture_update_callback,
gpointer texture_update_callback_context);
/**
* @brief Sets the required video output size. This forces |VideoOutput| to
* resize the internal OpenGL surface / texture.
*
* @param texture_registrar |FlTextureRegistrar| reference.
* @param width Preferred width of the video. Pass `NULL` for using texture
* dimensions based on video's resolution.
* @param height Preferred height of the video. Pass `NULL` for using texture
* dimensions based on video's resolution.
*/
void video_output_manager_set_size(VideoOutputManager* self,
gint64 handle,
gint64 width,
gint64 height);
/**
* @brief Disposes |VideoOutput| instance for given |handle|.
*
* @param self |VideoOutputManager| reference.
* @param handle |mpv_handle| reference casted to gint64.
*/
void video_output_manager_dispose(VideoOutputManager* self, gint64 handle);
#endif
@@ -0,0 +1,208 @@
// This file is a part of media_kit
// (https://github.com/media-kit/media-kit).
//
// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
// All rights reserved.
// Use of this source code is governed by MIT license that can be found in the
// LICENSE file.
#include "include/media_kit_video/media_kit_video_plugin.h"
#ifndef MEDIA_KIT_LIBS_NOT_FOUND
#include <gtk/gtk.h>
#include "include/media_kit_video/utils.h"
#include "include/media_kit_video/video_output_manager.h"
#define MEDIA_KIT_VIDEO_PLUGIN(obj) \
(G_TYPE_CHECK_INSTANCE_CAST((obj), media_kit_video_plugin_get_type(), \
MediaKitVideoPlugin))
struct _MediaKitVideoPlugin {
GObject parent_instance;
FlMethodChannel* channel;
FlView* view;
VideoOutputManager* video_output_manager;
};
G_DEFINE_TYPE(MediaKitVideoPlugin, media_kit_video_plugin, g_object_get_type())
static void media_kit_video_plugin_handle_method_call(
MediaKitVideoPlugin* self,
FlMethodCall* method_call) {
g_autoptr(FlMethodResponse) response = NULL;
const gchar* method = fl_method_call_get_name(method_call);
if (g_strcmp0(method, "VideoOutputManager.Create") == 0) {
FlValue* arguments = fl_method_call_get_args(method_call);
FlValue* handle = fl_value_lookup_string(arguments, "handle");
FlValue* configuration = fl_value_lookup_string(arguments, "configuration");
gint64 handle_value =
g_ascii_strtoll(fl_value_get_string(handle), NULL, 10);
VideoOutputConfiguration configuration_value = {};
const gchar* configuration_width =
fl_value_get_string(fl_value_lookup_string(configuration, "width"));
const gchar* configuration_height =
fl_value_get_string(fl_value_lookup_string(configuration, "height"));
const bool configuration_enable_hardware_acceleration = fl_value_get_bool(
fl_value_lookup_string(configuration, "enableHardwareAcceleration"));
if (g_strcmp0(configuration_width, "null") != 0) {
configuration_value.width =
g_ascii_strtoll(configuration_width, NULL, 10);
}
if (g_strcmp0(configuration_height, "null") != 0) {
configuration_value.height =
g_ascii_strtoll(configuration_height, NULL, 10);
}
configuration_value.enable_hardware_acceleration =
configuration_enable_hardware_acceleration;
typedef struct _VideoOutputTextureUpdateCallbackData {
FlMethodChannel* channel;
gint64 handle;
} VideoOutputTextureUpdateCallbackData;
// TODO(@alexmercerind): Fix memory leak.
VideoOutputTextureUpdateCallbackData* data =
g_new0(VideoOutputTextureUpdateCallbackData, 1);
data->channel = self->channel;
data->handle = handle_value;
video_output_manager_create(
self->video_output_manager, handle_value, configuration_value,
[](gint64 id, gint64 width, gint64 height, gpointer context) {
auto data = (VideoOutputTextureUpdateCallbackData*)context;
FlMethodChannel* channel = data->channel;
gint64 handle = data->handle;
FlValue* rect = fl_value_new_map();
fl_value_set_string_take(rect, "left", fl_value_new_int(0));
fl_value_set_string_take(rect, "top", fl_value_new_int(0));
fl_value_set_string_take(rect, "width", fl_value_new_int(width));
fl_value_set_string_take(rect, "height", fl_value_new_int(height));
FlValue* result = fl_value_new_map();
fl_value_set_string_take(result, "handle", fl_value_new_int(handle));
fl_value_set_string_take(result, "id", fl_value_new_int(id));
fl_value_set_string_take(result, "rect", rect);
typedef struct {
FlMethodChannel* channel;
FlValue* result;
} IdleCallbackData;
IdleCallbackData* idle_data = g_new0(IdleCallbackData, 1);
idle_data->channel = channel;
idle_data->result = result;
// `fl_method_channel_invoke_method` should be called from PlatformThread.
g_idle_add([](gpointer user_data) -> gboolean {
IdleCallbackData* idle_data = (IdleCallbackData*)user_data;
fl_method_channel_invoke_method(idle_data->channel, "VideoOutput.Resize",
idle_data->result, NULL, NULL, NULL);
g_free(idle_data);
return G_SOURCE_REMOVE;
}, idle_data);
},
data);
FlValue* result = fl_value_new_null();
response = FL_METHOD_RESPONSE(fl_method_success_response_new(result));
} else if (g_strcmp0(method, "VideoOutputManager.SetSize") == 0) {
FlValue* arguments = fl_method_call_get_args(method_call);
FlValue* handle = fl_value_lookup_string(arguments, "handle");
FlValue* width = fl_value_lookup_string(arguments, "width");
FlValue* height = fl_value_lookup_string(arguments, "height");
gint64 handle_value =
g_ascii_strtoll(fl_value_get_string(handle), NULL, 10);
gint64 width_value = 0;
gint64 height_value = 0;
if (g_strcmp0(fl_value_get_string(width), "null") != 0) {
width_value = g_ascii_strtoll(fl_value_get_string(width), NULL, 10);
}
if (g_strcmp0(fl_value_get_string(height), "null") != 0) {
height_value = g_ascii_strtoll(fl_value_get_string(height), NULL, 10);
}
video_output_manager_set_size(self->video_output_manager, handle_value,
width_value, height_value);
FlValue* result = fl_value_new_null();
response = FL_METHOD_RESPONSE(fl_method_success_response_new(result));
} else if (g_strcmp0(method, "VideoOutputManager.Dispose") == 0) {
FlValue* arguments = fl_method_call_get_args(method_call);
FlValue* handle = fl_value_lookup_string(arguments, "handle");
gint64 handle_value =
g_ascii_strtoll(fl_value_get_string(handle), NULL, 10);
video_output_manager_dispose(self->video_output_manager, handle_value);
FlValue* result = fl_value_new_null();
response = FL_METHOD_RESPONSE(fl_method_success_response_new(result));
} else if (g_strcmp0(method, "Utils.EnterNativeFullscreen") == 0) {
utils_enter_native_fullscreen(
gtk_widget_get_toplevel(GTK_WIDGET(self->view)));
FlValue* result = fl_value_new_null();
response = FL_METHOD_RESPONSE(fl_method_success_response_new(result));
} else if (g_strcmp0(method, "Utils.ExitNativeFullscreen") == 0) {
utils_exit_native_fullscreen(
gtk_widget_get_toplevel(GTK_WIDGET(self->view)));
FlValue* result = fl_value_new_null();
response = FL_METHOD_RESPONSE(fl_method_success_response_new(result));
} else {
response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new());
}
fl_method_call_respond(method_call, response, nullptr);
}
static void media_kit_video_plugin_dispose(GObject* object) {
G_OBJECT_CLASS(media_kit_video_plugin_parent_class)->dispose(object);
}
static void media_kit_video_plugin_class_init(MediaKitVideoPluginClass* klass) {
G_OBJECT_CLASS(klass)->dispose = media_kit_video_plugin_dispose;
}
static void media_kit_video_plugin_init(MediaKitVideoPlugin* self) {
self->channel = NULL;
self->video_output_manager = NULL;
}
static void method_call_cb(FlMethodChannel* channel,
FlMethodCall* method_call,
gpointer user_data) {
MediaKitVideoPlugin* plugin = MEDIA_KIT_VIDEO_PLUGIN(user_data);
media_kit_video_plugin_handle_method_call(plugin, method_call);
}
static MediaKitVideoPlugin* media_kit_video_plugin_new(
FlPluginRegistrar* registrar) {
MediaKitVideoPlugin* self = MEDIA_KIT_VIDEO_PLUGIN(
g_object_new(media_kit_video_plugin_get_type(), nullptr));
g_autoptr(FlMethodCodec) codec =
FL_METHOD_CODEC(fl_standard_method_codec_new());
self->channel =
fl_method_channel_new(fl_plugin_registrar_get_messenger(registrar),
"com.alexmercerind/media_kit_video", codec);
fl_method_channel_set_method_call_handler(self->channel, method_call_cb, self,
g_object_unref);
FlTextureRegistrar* texture_registrar =
fl_plugin_registrar_get_texture_registrar(registrar);
FlView* view = fl_plugin_registrar_get_view(registrar);
self->view = view;
self->video_output_manager =
video_output_manager_new(texture_registrar, view);
return self;
}
void media_kit_video_plugin_register_with_registrar(
FlPluginRegistrar* registrar) {
media_kit_video_plugin_new(registrar);
}
#else
#include <iostream>
void media_kit_video_plugin_register_with_registrar(
FlPluginRegistrar* registrar) {
std::cout << "media_kit: WARNING: package:media_kit_libs_*** not found."
<< std::endl;
}
#endif
+284
View File
@@ -0,0 +1,284 @@
// This file is a part of media_kit
// (https://github.com/media-kit/media-kit).
//
// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
// All rights reserved.
// Use of this source code is governed by MIT license that can be found in the
// LICENSE file.
#include "include/media_kit_video/texture_gl.h"
#include <epoxy/gl.h>
#include <epoxy/egl.h>
// EGLImage extension function pointers
typedef EGLImageKHR (*PFNEGLCREATEIMAGEKHRPROC)(EGLDisplay dpy, EGLContext ctx, EGLenum target, EGLClientBuffer buffer, const EGLint *attrib_list);
typedef EGLBoolean (*PFNEGLDESTROYIMAGEKHRPROC)(EGLDisplay dpy, EGLImageKHR image);
typedef void (*PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)(GLenum target, GLeglImageOES image);
// Define the extension functions
#ifndef eglCreateImageKHR
static PFNEGLCREATEIMAGEKHRPROC eglCreateImageKHR = NULL;
#endif
#ifndef eglDestroyImageKHR
static PFNEGLDESTROYIMAGEKHRPROC eglDestroyImageKHR = NULL;
#endif
#ifndef glEGLImageTargetTexture2DOES
static PFNGLEGLIMAGETARGETTEXTURE2DOESPROC glEGLImageTargetTexture2DOES = NULL;
#endif
static void init_egl_image_extensions() {
static gboolean initialized = FALSE;
if (!initialized) {
eglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR");
eglDestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR");
glEGLImageTargetTexture2DOES = (PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES");
initialized = TRUE;
}
}
struct _TextureGL {
FlTextureGL parent_instance;
guint32 name; // Flutter's texture name
guint32 fbo; // mpv's FBO
guint32 mpv_texture; // mpv's texture
EGLImageKHR egl_image; // EGLImage for sharing between contexts
guint32 current_width;
guint32 current_height;
VideoOutput* video_output;
};
G_DEFINE_TYPE(TextureGL, texture_gl, fl_texture_gl_get_type())
static void texture_gl_init(TextureGL* self) {
self->name = 0;
self->fbo = 0;
self->mpv_texture = 0;
self->egl_image = EGL_NO_IMAGE_KHR;
self->current_width = 1;
self->current_height = 1;
self->video_output = NULL;
}
static void texture_gl_dispose(GObject* object) {
TextureGL* self = TEXTURE_GL(object);
VideoOutput* video_output = self->video_output;
VideoOutputGlxState glx_state;
video_output_glx_release(&glx_state);
// Save current context
EGLDisplay current_display = eglGetCurrentDisplay();
EGLContext current_context = eglGetCurrentContext();
EGLSurface current_draw = eglGetCurrentSurface(EGL_DRAW);
EGLSurface current_read = eglGetCurrentSurface(EGL_READ);
// Clean up Flutter's texture (in Flutter's context)
if (self->name != 0) {
glDeleteTextures(1, &self->name);
self->name = 0;
}
// Clean up EGLImage
if (self->egl_image != EGL_NO_IMAGE_KHR && video_output != NULL) {
EGLDisplay egl_display = video_output_get_egl_display(video_output);
eglDestroyImageKHR(egl_display, self->egl_image);
self->egl_image = EGL_NO_IMAGE_KHR;
}
// Clean up mpv's OpenGL resources (in mpv's isolated context)
if (video_output != NULL) {
EGLDisplay egl_display = video_output_get_egl_display(video_output);
EGLContext egl_context = video_output_get_egl_context(video_output);
if (egl_context != EGL_NO_CONTEXT) {
eglMakeCurrent(egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, egl_context);
if (self->mpv_texture != 0) {
glDeleteTextures(1, &self->mpv_texture);
self->mpv_texture = 0;
}
if (self->fbo != 0) {
glDeleteFramebuffers(1, &self->fbo);
self->fbo = 0;
}
// Restore previous context
video_output_restore_egl_context(egl_display, current_display,
current_draw, current_read,
current_context);
}
}
video_output_glx_restore(&glx_state);
self->current_width = 1;
self->current_height = 1;
self->video_output = NULL;
G_OBJECT_CLASS(texture_gl_parent_class)->dispose(object);
}
static void texture_gl_class_init(TextureGLClass* klass) {
FL_TEXTURE_GL_CLASS(klass)->populate = texture_gl_populate_texture;
G_OBJECT_CLASS(klass)->dispose = texture_gl_dispose;
}
TextureGL* texture_gl_new(VideoOutput* video_output) {
init_egl_image_extensions();
TextureGL* self = TEXTURE_GL(g_object_new(texture_gl_get_type(), NULL));
self->video_output = video_output;
return self;
}
gboolean texture_gl_populate_texture(FlTextureGL* texture,
guint32* target,
guint32* name,
guint32* width,
guint32* height,
GError** error) {
TextureGL* self = TEXTURE_GL(texture);
VideoOutput* video_output = self->video_output;
gint32 required_width = (guint32)video_output_get_width(video_output);
gint32 required_height = (guint32)video_output_get_height(video_output);
if (required_width > 0 && required_height > 0) {
gboolean first_frame = self->name == 0 || self->fbo == 0 || self->mpv_texture == 0;
gboolean resize = self->current_width != required_width ||
self->current_height != required_height;
if (first_frame || resize) {
// Save Flutter's current EGL context
EGLDisplay flutter_display = eglGetCurrentDisplay();
EGLContext flutter_context = eglGetCurrentContext();
EGLSurface flutter_draw = eglGetCurrentSurface(EGL_DRAW);
EGLSurface flutter_read = eglGetCurrentSurface(EGL_READ);
EGLDisplay egl_display = video_output_get_egl_display(video_output);
EGLContext egl_context = video_output_get_egl_context(video_output);
// Switch to mpv's isolated context to create/resize mpv's texture and FBO
eglMakeCurrent(egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, egl_context);
// Free previous resources in mpv's context
if (!first_frame) {
glDeleteTextures(1, &self->mpv_texture);
glDeleteFramebuffers(1, &self->fbo);
if (self->egl_image != EGL_NO_IMAGE_KHR) {
eglDestroyImageKHR(egl_display, self->egl_image);
}
}
// Create mpv's FBO and texture
glGenFramebuffers(1, &self->fbo);
glBindFramebuffer(GL_FRAMEBUFFER, self->fbo);
glGenTextures(1, &self->mpv_texture);
glBindTexture(GL_TEXTURE_2D, self->mpv_texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, required_width, required_height,
0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
// Attach mpv's texture to FBO
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D, self->mpv_texture, 0);
// Create EGLImage from mpv's texture
EGLint egl_image_attribs[] = { EGL_NONE };
self->egl_image = eglCreateImageKHR(
egl_display,
egl_context,
EGL_GL_TEXTURE_2D_KHR,
(EGLClientBuffer)(guintptr)self->mpv_texture,
egl_image_attribs);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glBindTexture(GL_TEXTURE_2D, 0);
// Flush to ensure mpv's texture is ready
glFlush();
// Switch back to Flutter's context to create/update Flutter's texture
eglMakeCurrent(flutter_display, flutter_draw, flutter_read, flutter_context);
// Free previous Flutter texture
if (!first_frame && self->name != 0) {
glDeleteTextures(1, &self->name);
}
// Create Flutter's texture from EGLImage
glGenTextures(1, &self->name);
glBindTexture(GL_TEXTURE_2D, self->name);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, self->egl_image);
glBindTexture(GL_TEXTURE_2D, 0);
self->current_width = required_width;
self->current_height = required_height;
// Notify Flutter about dimension change
video_output_notify_texture_update(video_output);
// Flutter's context is already current, so we're ready to render
}
// Save Flutter's current context
EGLDisplay flutter_display = eglGetCurrentDisplay();
EGLContext flutter_context = eglGetCurrentContext();
EGLSurface flutter_draw = eglGetCurrentSurface(EGL_DRAW);
EGLSurface flutter_read = eglGetCurrentSurface(EGL_READ);
EGLDisplay egl_display = video_output_get_egl_display(video_output);
EGLContext egl_context = video_output_get_egl_context(video_output);
mpv_render_context* render_context = video_output_get_render_context(video_output);
// Switch to mpv's isolated context for rendering
eglMakeCurrent(egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, egl_context);
// Bind mpv's FBO
glBindFramebuffer(GL_FRAMEBUFFER, self->fbo);
// Render mpv frame to mpv's texture
mpv_opengl_fbo fbo{(gint32)self->fbo, required_width, required_height, 0};
int flip_y = 0;
mpv_render_param params[] = {
{MPV_RENDER_PARAM_OPENGL_FBO, &fbo},
{MPV_RENDER_PARAM_FLIP_Y, &flip_y},
{MPV_RENDER_PARAM_INVALID, NULL},
};
mpv_render_context_render(render_context, params);
// Unbind FBO
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// Flush to ensure rendering is complete
glFlush();
// Restore Flutter's context
eglMakeCurrent(flutter_display, flutter_draw, flutter_read, flutter_context);
}
*target = GL_TEXTURE_2D;
*name = self->name;
*width = self->current_width;
*height = self->current_height;
if (self->name == 0) {
// First frame not yet available - create dummy texture in Flutter's context
glGenTextures(1, &self->name);
glBindTexture(GL_TEXTURE_2D, self->name);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
glBindTexture(GL_TEXTURE_2D, 0);
*name = self->name;
*width = 1;
*height = 1;
}
return TRUE;
}
+64
View File
@@ -0,0 +1,64 @@
// This file is a part of media_kit
// (https://github.com/media-kit/media-kit).
//
// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
// All rights reserved.
// Use of this source code is governed by MIT license that can be found in the
// LICENSE file.
#include "include/media_kit_video/texture_sw.h"
struct _TextureSW {
FlPixelBufferTexture parent_instance;
guint32 current_width;
guint32 current_height;
VideoOutput* video_output;
};
G_DEFINE_TYPE(TextureSW, texture_sw, fl_pixel_buffer_texture_get_type())
static void texture_sw_init(TextureSW* self) {
self->current_width = 1;
self->current_height = 1;
self->video_output = NULL;
}
static void texture_sw_dispose(GObject* object) {
G_OBJECT_CLASS(texture_sw_parent_class)->dispose(object);
}
static void texture_sw_class_init(TextureSWClass* klass) {
FL_PIXEL_BUFFER_TEXTURE_CLASS(klass)->copy_pixels = texture_sw_copy_pixels;
G_OBJECT_CLASS(klass)->dispose = texture_sw_dispose;
}
TextureSW* texture_sw_new(VideoOutput* video_output) {
TextureSW* self = TEXTURE_SW(g_object_new(texture_sw_get_type(), NULL));
self->video_output = video_output;
return self;
}
gboolean texture_sw_copy_pixels(FlPixelBufferTexture* texture,
const guint8** buffer,
guint32* width,
guint32* height,
GError** error) {
TextureSW* self = TEXTURE_SW(texture);
VideoOutput* video_output = self->video_output;
gint32 required_width = (guint32)video_output_get_width(video_output);
gint32 required_height = (guint32)video_output_get_height(video_output);
if (required_width > 0 && required_height > 0) {
guint8* pixel_buffer = video_output_get_pixel_buffer(video_output);
if (self->current_width != required_width ||
self->current_height != required_height) {
self->current_width = required_width;
self->current_height = required_height;
// Notify Flutter about the change in texture's dimensions.
video_output_notify_texture_update(video_output);
}
*buffer = pixel_buffer;
*width = required_width;
*height = required_height;
}
return TRUE;
}
+18
View File
@@ -0,0 +1,18 @@
// // This file is a part of media_kit
// // (https://github.com/media-kit/media-kit).
// //
// // Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
// // All rights reserved.
// // Use of this source code is governed by MIT license that can be found in
// the
// // LICENSE file.
#include "include/media_kit_video/utils.h"
void utils_enter_native_fullscreen(GtkWidget* window) {
gtk_window_fullscreen(GTK_WINDOW(window));
}
void utils_exit_native_fullscreen(GtkWidget* window) {
gtk_window_unfullscreen(GTK_WINDOW(window));
}
+586
View File
@@ -0,0 +1,586 @@
// This file is a part of media_kit
// (https://github.com/media-kit/media-kit).
//
// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
// All rights reserved.
// Use of this source code is governed by MIT license that can be found in the
// LICENSE file.
#include "include/media_kit_video/video_output.h"
#include "include/media_kit_video/texture_gl.h"
#include "include/media_kit_video/texture_sw.h"
#include <epoxy/egl.h>
#include <epoxy/glx.h>
#include <gdk/gdkwayland.h>
#include <gdk/gdkx.h>
// Flutter >= 3.38 keeps its EGL contexts on the raster thread, so nothing is
// current on the platform thread (where |video_output_new| runs). Open the
// display the way Flutter's FlOpenGLManager does: EGL hands back the same
// EGLDisplay for the same platform and native display, which the EGLImage
// shared in |texture_gl_populate_texture| needs. eglGetPlatformDisplayEXT is
// looked up by hand: epoxy's dispatch for it needs a current display.
static EGLDisplay flutter_egl_display() {
typedef EGLDisplay (*GetPlatformDisplay)(EGLenum, void*, const EGLint*);
GetPlatformDisplay get_platform_display =
(GetPlatformDisplay)eglGetProcAddress("eglGetPlatformDisplayEXT");
if (get_platform_display == NULL) {
return EGL_NO_DISPLAY;
}
EGLDisplay egl_display = EGL_NO_DISPLAY;
GdkDisplay* display = gdk_display_get_default();
if (GDK_IS_WAYLAND_DISPLAY(display)) {
egl_display = get_platform_display(
EGL_PLATFORM_WAYLAND_EXT, gdk_wayland_display_get_wl_display(display),
NULL);
} else if (GDK_IS_X11_DISPLAY(display)) {
egl_display = get_platform_display(
EGL_PLATFORM_X11_EXT, gdk_x11_display_get_xdisplay(display), NULL);
}
if (egl_display != EGL_NO_DISPLAY &&
!eglInitialize(egl_display, NULL, NULL)) {
egl_display = EGL_NO_DISPLAY;
}
return egl_display;
}
void video_output_glx_release(VideoOutputGlxState* state) {
*state = VideoOutputGlxState{};
// On X11, GDK draws Flutter's view with GLX on the platform thread, and
// libglvnd refuses an EGL context (EGL_BAD_ACCESS) on a thread holding a
// GLX one: step it aside while ours is in use.
if (!GDK_IS_X11_DISPLAY(gdk_display_get_default()) ||
glXGetCurrentContext() == NULL) {
return;
}
state->display = glXGetCurrentDisplay();
state->draw = glXGetCurrentDrawable();
state->read = glXGetCurrentReadDrawable();
state->context = glXGetCurrentContext();
glXMakeContextCurrent(state->display, None, None, NULL);
}
void video_output_glx_restore(VideoOutputGlxState* state) {
if (state->context != NULL) {
glXMakeContextCurrent(state->display, state->draw, state->read,
state->context);
}
}
void video_output_restore_egl_context(EGLDisplay own_display,
EGLDisplay display,
EGLSurface draw,
EGLSurface read,
EGLContext context) {
// Nothing was current (the platform thread): release ours, or it stays
// current here and the raster thread cannot make it current (BadAccess).
if (context == EGL_NO_CONTEXT) {
eglMakeCurrent(own_display, EGL_NO_SURFACE, EGL_NO_SURFACE,
EGL_NO_CONTEXT);
} else {
eglMakeCurrent(display, draw, read, context);
}
}
struct _VideoOutput {
GObject parent_instance;
TextureGL* texture_gl;
EGLDisplay egl_display; /* EGL display for mpv rendering (shared with flutter). */
EGLContext egl_context; /* Isolated EGL context (non-shared). */
EGLSurface egl_surface; /* Place holder surface for activating egl context */
guint8* pixel_buffer;
TextureSW* texture_sw;
GMutex mutex; /* Only used in S/W rendering. */
mpv_handle* handle;
mpv_render_context* render_context;
gint64 width;
gint64 height;
VideoOutputConfiguration configuration;
TextureUpdateCallback texture_update_callback;
gpointer texture_update_callback_context;
FlTextureRegistrar* texture_registrar;
gboolean destroyed;
};
G_DEFINE_TYPE(VideoOutput, video_output, G_TYPE_OBJECT)
static void video_output_dispose(GObject* object) {
VideoOutput* self = VIDEO_OUTPUT(object);
self->destroyed = TRUE;
// Make sure that no more callbacks are invoked from mpv.
if (self->render_context) {
mpv_render_context_set_update_callback(self->render_context, NULL, NULL);
}
// H/W
if (self->texture_gl) {
fl_texture_registrar_unregister_texture(self->texture_registrar,
FL_TEXTURE(self->texture_gl));
// Save Flutter's current context before cleanup
EGLDisplay current_display = eglGetCurrentDisplay();
EGLContext flutter_context = eglGetCurrentContext();
EGLSurface flutter_draw_surface = eglGetCurrentSurface(EGL_DRAW);
EGLSurface flutter_read_surface = eglGetCurrentSurface(EGL_READ);
// Free mpv_render_context with our own isolated EGL context
if (self->render_context != NULL) {
VideoOutputGlxState glx_state;
video_output_glx_release(&glx_state);
if (self->egl_context != EGL_NO_CONTEXT) {
eglMakeCurrent(self->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, self->egl_context);
}
mpv_render_context_free(self->render_context);
self->render_context = NULL;
// Restore Flutter's context
video_output_restore_egl_context(self->egl_display, current_display,
flutter_draw_surface,
flutter_read_surface, flutter_context);
video_output_glx_restore(&glx_state);
}
// Clean up EGL resources
if (self->egl_context != EGL_NO_CONTEXT) {
eglDestroyContext(self->egl_display, self->egl_context);
self->egl_context = EGL_NO_CONTEXT;
}
g_object_unref(self->texture_gl);
}
// S/W
if (self->texture_sw) {
fl_texture_registrar_unregister_texture(self->texture_registrar,
FL_TEXTURE(self->texture_sw));
g_free(self->pixel_buffer);
g_object_unref(self->texture_sw);
if (self->render_context != NULL) {
mpv_render_context_free(self->render_context);
self->render_context = NULL;
}
}
g_mutex_clear(&self->mutex);
G_OBJECT_CLASS(video_output_parent_class)->dispose(object);
}
static void video_output_class_init(VideoOutputClass* klass) {
G_OBJECT_CLASS(klass)->dispose = video_output_dispose;
}
static void video_output_init(VideoOutput* self) {
self->texture_gl = NULL;
self->egl_display = EGL_NO_DISPLAY;
self->egl_context = EGL_NO_CONTEXT;
self->egl_surface = EGL_NO_SURFACE;
self->texture_sw = NULL;
self->pixel_buffer = NULL;
self->handle = NULL;
self->render_context = NULL;
self->width = 0;
self->height = 0;
self->configuration = VideoOutputConfiguration{};
self->texture_update_callback = NULL;
self->texture_update_callback_context = NULL;
self->texture_registrar = NULL;
self->destroyed = FALSE;
g_mutex_init(&self->mutex);
}
VideoOutput* video_output_new(FlTextureRegistrar* texture_registrar,
FlView* view,
gint64 handle,
VideoOutputConfiguration configuration) {
VideoOutput* self = VIDEO_OUTPUT(g_object_new(video_output_get_type(), NULL));
self->texture_registrar = texture_registrar;
self->handle = (mpv_handle*)handle;
self->width = configuration.width;
self->height = configuration.height;
self->configuration = configuration;
#ifndef MPV_RENDER_API_TYPE_SW
// MPV_RENDER_API_TYPE_SW must be available for S/W rendering.
if (!self->configuration.enable_hardware_acceleration) {
g_printerr("media_kit: VideoOutput: S/W rendering is not supported.\n");
}
self->configuration.enable_hardware_acceleration = TRUE;
#endif
mpv_set_option_string(self->handle, "video-sync", "audio");
// Causes frame drops with `pulse` audio output. (SlotSun/dart_simple_live#42)
// mpv_set_option_string(self->handle, "video-timing-offset", "0");
gboolean hardware_acceleration_supported = FALSE;
if (self->configuration.enable_hardware_acceleration) {
VideoOutputGlxState glx_state;
video_output_glx_release(&glx_state);
// Get Flutter's current EGL display (DO NOT share context)
EGLDisplay flutter_display = eglGetCurrentDisplay();
EGLContext flutter_context = eglGetCurrentContext();
EGLSurface flutter_draw_surface = eglGetCurrentSurface(EGL_DRAW);
EGLSurface flutter_read_surface = eglGetCurrentSurface(EGL_READ);
EGLDisplay egl_display = flutter_context != EGL_NO_CONTEXT
? flutter_display
: flutter_egl_display();
if (egl_display != EGL_NO_DISPLAY) {
self->egl_display = egl_display;
// Bind OpenGL ES API (Flutter uses OpenGL ES on Linux)
eglBindAPI(EGL_OPENGL_ES_API);
// Query Flutter's EGL config and reuse it for compatibility
EGLConfig config = NULL;
EGLint config_id = 0;
if (flutter_context == EGL_NO_CONTEXT) {
// The attributes FlOpenGLManager chooses Flutter's config with.
EGLint num_configs = 0;
const EGLint config_attribs[] = {
EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8,
EGL_ALPHA_SIZE, 8, EGL_DEPTH_SIZE, 8, EGL_STENCIL_SIZE, 8,
EGL_NONE,
};
if (!eglChooseConfig(self->egl_display, config_attribs, &config, 1,
&num_configs) ||
num_configs == 0) {
g_printerr("media_kit: VideoOutput: Failed to choose an EGL config.\n");
config = NULL;
}
} else if (eglQueryContext(self->egl_display, flutter_context, EGL_CONFIG_ID, &config_id)) {
g_print("media_kit: VideoOutput: Flutter's EGL config ID: %d\n", config_id);
// Get Flutter's exact config
EGLint num_configs = 0;
EGLint config_attribs[] = { EGL_CONFIG_ID, config_id, EGL_NONE };
if (eglChooseConfig(self->egl_display, config_attribs, &config, 1, &num_configs) && num_configs > 0) {
g_print("media_kit: VideoOutput: Using Flutter's EGL config.\n");
} else {
g_printerr("media_kit: VideoOutput: Failed to get Flutter's EGL config by ID.\n");
config = NULL;
}
} else {
g_printerr("media_kit: VideoOutput: Failed to query Flutter's EGL config ID.\n");
}
if (config != NULL) {
// Create an isolated EGL context (NOT shared with Flutter)
// This prevents OpenGL state pollution and resource contention
EGLint context_attribs[] = {
EGL_CONTEXT_CLIENT_VERSION, 2,
EGL_NONE,
};
self->egl_context = eglCreateContext(self->egl_display, config,
EGL_NO_CONTEXT, context_attribs);
if (self->egl_context != EGL_NO_CONTEXT) {
// Make our isolated context current for initialization (surfaceless)
if (eglMakeCurrent(self->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, self->egl_context)) {
// Create texture with our isolated context
self->texture_gl = texture_gl_new(self);
if (fl_texture_registrar_register_texture(
texture_registrar, FL_TEXTURE(self->texture_gl))) {
// Initialize mpv with our isolated EGL context
mpv_opengl_init_params gl_init_params{
[](auto, auto name) {
return (void*)eglGetProcAddress(name);
},
NULL,
};
mpv_render_param params[] = {
{MPV_RENDER_PARAM_API_TYPE, (void*)MPV_RENDER_API_TYPE_OPENGL},
{MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, (void*)&gl_init_params},
{MPV_RENDER_PARAM_INVALID, (void*)0},
{MPV_RENDER_PARAM_INVALID, (void*)0},
};
// VAAPI acceleration requires passing X11/Wayland display
GdkDisplay* display = gdk_display_get_default();
if (GDK_IS_WAYLAND_DISPLAY(display)) {
params[2].type = MPV_RENDER_PARAM_WL_DISPLAY;
params[2].data = gdk_wayland_display_get_wl_display(display);
} else if (GDK_IS_X11_DISPLAY(display)) {
params[2].type = MPV_RENDER_PARAM_X11_DISPLAY;
params[2].data = gdk_x11_display_get_xdisplay(display);
}
if (mpv_render_context_create(&self->render_context, self->handle, params) == 0) {
mpv_render_context_set_update_callback(
self->render_context,
[](void* data) {
VideoOutput* self = (VideoOutput*)data;
if (self->destroyed) {
return;
}
fl_texture_registrar_mark_texture_frame_available(
self->texture_registrar, FL_TEXTURE(self->texture_gl));
},
self);
hardware_acceleration_supported = TRUE;
g_print("media_kit: VideoOutput: H/W rendering with isolated EGL context.\n");
} else {
g_printerr("media_kit: VideoOutput: Failed to create mpv_render_context.\n");
}
} else {
g_printerr("media_kit: VideoOutput: Failed to register texture.\n");
}
// Restore Flutter's context
video_output_restore_egl_context(
self->egl_display, flutter_display, flutter_draw_surface,
flutter_read_surface, flutter_context);
} else {
g_printerr("media_kit: VideoOutput: Failed to make isolated EGL context current. Error: 0x%x\n", eglGetError());
}
} else {
g_printerr("media_kit: VideoOutput: Failed to create isolated EGL context. Error: 0x%x\n", eglGetError());
}
} else {
g_printerr("media_kit: VideoOutput: Could not obtain Flutter's EGL config.\n");
}
} else {
g_printerr("media_kit: VideoOutput: EGL display is invalid.\n");
}
video_output_glx_restore(&glx_state);
}
#ifdef MPV_RENDER_API_TYPE_SW
if (!hardware_acceleration_supported) {
g_printerr("media_kit: VideoOutput: S/W rendering.\n");
// H/W rendering failed. Fallback to S/W rendering.
self->pixel_buffer = g_new0(guint8, SW_RENDERING_PIXEL_BUFFER_SIZE);
self->texture_gl = NULL;
self->texture_sw = texture_sw_new(self);
if (fl_texture_registrar_register_texture(texture_registrar,
FL_TEXTURE(self->texture_sw))) {
mpv_render_param params[] = {
{MPV_RENDER_PARAM_API_TYPE, (void*)MPV_RENDER_API_TYPE_SW},
{MPV_RENDER_PARAM_INVALID, (void*)0},
};
if (mpv_render_context_create(&self->render_context, self->handle,
params) == 0) {
mpv_render_context_set_update_callback(
self->render_context,
[](void* data) {
gdk_threads_add_idle(
[](gpointer data) -> gboolean {
VideoOutput* self = (VideoOutput*)data;
if (self->destroyed) {
return FALSE;
}
g_mutex_lock(&self->mutex);
gint64 width = video_output_get_width(self);
gint64 height = video_output_get_height(self);
if (width > 0 && height > 0) {
gint32 size[]{(gint32)width, (gint32)height};
gint32 pitch = 4 * (gint32)width;
mpv_render_param params[]{
{MPV_RENDER_PARAM_SW_SIZE, size},
{MPV_RENDER_PARAM_SW_FORMAT, (void*)"rgb0"},
{MPV_RENDER_PARAM_SW_STRIDE, &pitch},
{MPV_RENDER_PARAM_SW_POINTER, self->pixel_buffer},
{MPV_RENDER_PARAM_INVALID, (void*)0},
};
mpv_render_context_render(self->render_context, params);
fl_texture_registrar_mark_texture_frame_available(
self->texture_registrar,
FL_TEXTURE(self->texture_sw));
}
g_mutex_unlock(&self->mutex);
return FALSE;
},
data);
},
self);
}
}
}
#endif
return self;
}
void video_output_set_texture_update_callback(
VideoOutput* self,
TextureUpdateCallback texture_update_callback,
gpointer texture_update_callback_context) {
self->texture_update_callback = texture_update_callback;
self->texture_update_callback_context = texture_update_callback_context;
// Notify initial dimensions as (1, 1) if |width| & |height| are 0 i.e.
// texture & video frame size is based on playing file's resolution. This
// will make sure that `Texture` widget on Flutter's widget tree is actually
// mounted & |fl_texture_registrar_mark_texture_frame_available| actually
// invokes the |TextureGL| or |TextureSW| callbacks. Otherwise it will be a
// never ending deadlock where no video frames are ever rendered.
gint64 texture_id = video_output_get_texture_id(self);
if (self->width == 0 || self->height == 0) {
self->texture_update_callback(texture_id, 1, 1,
self->texture_update_callback_context);
} else {
self->texture_update_callback(texture_id, self->width, self->height,
self->texture_update_callback_context);
}
}
void video_output_set_size(VideoOutput* self, gint64 width, gint64 height) {
// Ideally, a mutex should be used here & |video_output_get_width| +
// |video_output_get_height|. However, that is throwing everything into a
// deadlock. Flutter itself seems to have some synchronization mechanism in
// rendering & platform channels AFAIK.
// H/W
if (self->texture_gl) {
self->width = width;
self->height = height;
}
// S/W
if (self->texture_sw) {
self->width = CLAMP(width, 0, SW_RENDERING_MAX_WIDTH);
self->height = CLAMP(height, 0, SW_RENDERING_MAX_HEIGHT);
}
}
mpv_render_context* video_output_get_render_context(VideoOutput* self) {
return self->render_context;
}
EGLDisplay video_output_get_egl_display(VideoOutput* self) {
return self->egl_display;
}
EGLContext video_output_get_egl_context(VideoOutput* self) {
return self->egl_context;
}
EGLSurface video_output_get_egl_surface(VideoOutput* self) {
return self->egl_surface;
}
guint8* video_output_get_pixel_buffer(VideoOutput* self) {
return self->pixel_buffer;
}
gint64 video_output_get_width(VideoOutput* self) {
// Fixed width.
if (self->width) {
return self->width;
}
// Video resolution dependent width.
gint64 width = 0;
gint64 height = 0;
mpv_node params;
mpv_get_property(self->handle, "video-out-params", MPV_FORMAT_NODE, &params);
int64_t dw = 0, dh = 0, rotate = 0;
if (params.format == MPV_FORMAT_NODE_MAP) {
for (int32_t i = 0; i < params.u.list->num; i++) {
char* key = params.u.list->keys[i];
auto value = params.u.list->values[i];
if (value.format == MPV_FORMAT_INT64) {
if (strcmp(key, "dw") == 0) {
dw = value.u.int64;
}
if (strcmp(key, "dh") == 0) {
dh = value.u.int64;
}
if (strcmp(key, "rotate") == 0) {
rotate = value.u.int64;
}
}
}
mpv_free_node_contents(&params);
}
width = rotate == 0 || rotate == 180 ? dw : dh;
height = rotate == 0 || rotate == 180 ? dh : dw;
if (self->texture_sw != NULL) {
// Make sure |width| & |height| fit between |SW_RENDERING_MAX_WIDTH| &
// |SW_RENDERING_MAX_HEIGHT| while maintaining aspect ratio.
if (width >= SW_RENDERING_MAX_WIDTH) {
return SW_RENDERING_MAX_WIDTH;
}
if (height >= SW_RENDERING_MAX_HEIGHT) {
return width / height * SW_RENDERING_MAX_HEIGHT;
}
}
return width;
}
gint64 video_output_get_height(VideoOutput* self) {
// Fixed height.
if (self->width) {
return self->height;
}
// Video resolution dependent height.
gint64 width = 0;
gint64 height = 0;
mpv_node params;
mpv_get_property(self->handle, "video-out-params", MPV_FORMAT_NODE, &params);
int64_t dw = 0, dh = 0, rotate = 0;
if (params.format == MPV_FORMAT_NODE_MAP) {
for (int32_t i = 0; i < params.u.list->num; i++) {
char* key = params.u.list->keys[i];
auto value = params.u.list->values[i];
if (value.format == MPV_FORMAT_INT64) {
if (strcmp(key, "dw") == 0) {
dw = value.u.int64;
}
if (strcmp(key, "dh") == 0) {
dh = value.u.int64;
}
if (strcmp(key, "rotate") == 0) {
rotate = value.u.int64;
}
}
}
mpv_free_node_contents(&params);
}
width = rotate == 0 || rotate == 180 ? dw : dh;
height = rotate == 0 || rotate == 180 ? dh : dw;
if (self->texture_sw != NULL) {
// Make sure |width| & |height| fit between |SW_RENDERING_MAX_WIDTH| &
// |SW_RENDERING_MAX_HEIGHT| while maintaining aspect ratio.
if (height >= SW_RENDERING_MAX_HEIGHT) {
return SW_RENDERING_MAX_HEIGHT;
}
if (width >= SW_RENDERING_MAX_WIDTH) {
return height / width * SW_RENDERING_MAX_WIDTH;
}
}
return height;
}
gint64 video_output_get_texture_id(VideoOutput* self) {
// H/W
if (self->texture_gl) {
return (gint64)self->texture_gl;
}
// S/W
if (self->texture_sw) {
return (gint64)self->texture_sw;
}
g_assert_not_reached();
return -1;
}
void video_output_notify_texture_update(VideoOutput* self) {
gint64 id = video_output_get_texture_id(self);
gint64 width = video_output_get_width(self);
gint64 height = video_output_get_height(self);
gpointer context = self->texture_update_callback_context;
if (self->texture_update_callback != NULL) {
self->texture_update_callback(id, width, height, context);
}
}
@@ -0,0 +1,75 @@
// This file is a part of media_kit
// (https://github.com/media-kit/media-kit).
//
// Copyright © 2021 & onwards, Hitesh Kumar Saini <saini123hitesh@gmail.com>.
// All rights reserved.
// Use of this source code is governed by MIT license that can be found in the
// LICENSE file.
#include "include/media_kit_video/video_output_manager.h"
struct _VideoOutputManager {
GObject parent_instance;
GHashTable* video_outputs;
FlTextureRegistrar* texture_registrar;
FlView* view;
};
G_DEFINE_TYPE(VideoOutputManager, video_output_manager, G_TYPE_OBJECT)
static void video_output_manager_init(VideoOutputManager* self) {
self->video_outputs = g_hash_table_new_full(g_direct_hash, g_direct_equal,
nullptr, g_object_unref);
}
static void video_output_manager_dispose(GObject* object) {
VideoOutputManager* self = VIDEO_OUTPUT_MANAGER(object);
g_hash_table_unref(self->video_outputs);
G_OBJECT_CLASS(video_output_manager_parent_class)->dispose(object);
}
static void video_output_manager_class_init(VideoOutputManagerClass* klass) {
G_OBJECT_CLASS(klass)->dispose = video_output_manager_dispose;
}
VideoOutputManager* video_output_manager_new(
FlTextureRegistrar* texture_registrar,
FlView* view) {
VideoOutputManager* video_output_manager = VIDEO_OUTPUT_MANAGER(
g_object_new(video_output_manager_get_type(), nullptr));
video_output_manager->texture_registrar = texture_registrar;
video_output_manager->view = view;
return video_output_manager;
}
void video_output_manager_create(VideoOutputManager* self,
gint64 handle,
VideoOutputConfiguration configuration,
TextureUpdateCallback texture_update_callback,
gpointer texture_update_callback_context) {
if (!g_hash_table_contains(self->video_outputs, GINT_TO_POINTER(handle))) {
g_autoptr(VideoOutput) video_output = video_output_new(
self->texture_registrar, self->view, handle, configuration);
video_output_set_texture_update_callback(
video_output, texture_update_callback, texture_update_callback_context);
g_hash_table_insert(self->video_outputs, GINT_TO_POINTER(handle),
g_object_ref(video_output));
}
}
void video_output_manager_set_size(VideoOutputManager* self,
gint64 handle,
gint64 width,
gint64 height) {
if (g_hash_table_contains(self->video_outputs, GINT_TO_POINTER(handle))) {
VideoOutput* video_output = VIDEO_OUTPUT(
g_hash_table_lookup(self->video_outputs, GINT_TO_POINTER(handle)));
video_output_set_size(video_output, width, height);
}
}
void video_output_manager_dispose(VideoOutputManager* self, gint64 handle) {
if (g_hash_table_contains(self->video_outputs, GINT_TO_POINTER(handle))) {
g_hash_table_remove(self->video_outputs, GINT_TO_POINTER(handle));
}
}