feat(linux): restore the Flutter-texture path as the SDR fallback
2.13.0 deleted the display-agnostic EGL/Flutter-texture renderer and made the native Wayland plane the only path, hard-rejecting every session that cannot host one - X11, XWayland (SteamOS Gaming Mode runs native apps through Gamescope's XWayland), or a plane whose EGL bootstrap failed. This restores the 2.11.0 texture path from git history as the fallback: - Re-add mpv_texture.cc/.h and mpv_gpu_bootstrap.cc/.h (2.11.0 verbatim): an FlTextureGL whose populate renders mpv into an offscreen FBO sampled by Flutter via an EGL image. - MpvPlayer gains the texture-mode render-context API (InitRenderContext, HasRenderContext, GetEglDisplay/Context, Render(w,h,fbo)) beside the plane's InitRenderContextForSurface/RenderToSurface. The isolated ES 2.0 context on Flutter's display and the X11 display param are exactly the 2.11.0 configuration hardware decode demonstrably worked in. - initialize now tries the plane first and falls back to the texture path when it cannot be brought up, returning the texture id (Dart's 2.11.0 'result is int' contract) with waitForVideoReady gating playback until the GPU bootstrap settles. Both paths share one mpv core, so the plane/texture decision precedes render-context creation. - hdr-enabled/hdr-tone-mapping are intercepted in texture mode (no plane, no HDR); the HDR toggle hides itself via isHDRSupported. - New Linux setting 'Video rendering mode' (Automatic / Texture) forces the fallback - the user-visible workaround for plane-only trouble and for the hwdec interop regression, plus translations in all locales. SDR only on the fallback, matching 2.11.0; the plane path is unchanged.
This commit is contained in:
@@ -4,8 +4,10 @@ project(runner LANGUAGES CXX)
|
||||
add_executable(${BINARY_NAME}
|
||||
"main.cc"
|
||||
"my_application.cc"
|
||||
"mpv/mpv_gpu_bootstrap.cc"
|
||||
"mpv/mpv_player.cc"
|
||||
"mpv/mpv_plugin.cc"
|
||||
"mpv/mpv_texture.cc"
|
||||
"mpv/wayland_video_surface.cc"
|
||||
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
#include "mpv_gpu_bootstrap.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace mpv {
|
||||
namespace {
|
||||
|
||||
bool HasExtension(const char* extensions, const char* requested) {
|
||||
if (!extensions || !requested || requested[0] == '\0' || std::strchr(requested, ' ')) return false;
|
||||
const size_t requested_length = std::strlen(requested);
|
||||
const char* current = extensions;
|
||||
while ((current = std::strstr(current, requested)) != nullptr) {
|
||||
const bool starts_token = current == extensions || current[-1] == ' ';
|
||||
const char following = current[requested_length];
|
||||
if (starts_token && (following == '\0' || following == ' ')) return true;
|
||||
current += requested_length;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ParseEglVersion(const char* version, int* major, int* minor) {
|
||||
if (!version || !major || !minor) return false;
|
||||
char* end = nullptr;
|
||||
const long parsed_major = std::strtol(version, &end, 10);
|
||||
if (end == version || *end != '.') return false;
|
||||
const char* minor_start = end + 1;
|
||||
const long parsed_minor = std::strtol(minor_start, &end, 10);
|
||||
if (end == minor_start || parsed_major < 0 || parsed_minor < 0) return false;
|
||||
*major = static_cast<int>(parsed_major);
|
||||
*minor = static_cast<int>(parsed_minor);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AtLeastEgl15(const GpuBootstrapProbe& probe) {
|
||||
return probe.egl_major > 1 || (probe.egl_major == 1 && probe.egl_minor >= 5);
|
||||
}
|
||||
|
||||
bool Fail(std::string* error, const char* message) {
|
||||
if (error) *error = message;
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
EGLImageKHR GpuImageDispatch::Create(EGLDisplay display, EGLContext context, EGLClientBuffer buffer) const {
|
||||
if (uses_core) {
|
||||
if (!create_image_core) return EGL_NO_IMAGE_KHR;
|
||||
const EGLAttrib attributes[] = {EGL_NONE};
|
||||
return reinterpret_cast<EGLImageKHR>(
|
||||
create_image_core(display, context, EGL_GL_TEXTURE_2D_KHR, buffer, attributes));
|
||||
}
|
||||
if (!create_image_khr) return EGL_NO_IMAGE_KHR;
|
||||
const EGLint attributes[] = {EGL_NONE};
|
||||
return create_image_khr(display, context, EGL_GL_TEXTURE_2D_KHR, buffer, attributes);
|
||||
}
|
||||
|
||||
bool GpuImageDispatch::Destroy(EGLDisplay display, EGLImageKHR image) const {
|
||||
if (image == EGL_NO_IMAGE_KHR) return true;
|
||||
if (uses_core) {
|
||||
return destroy_image_core && destroy_image_core(display, reinterpret_cast<EGLImage>(image)) == EGL_TRUE;
|
||||
}
|
||||
return destroy_image_khr && destroy_image_khr(display, image) == EGL_TRUE;
|
||||
}
|
||||
|
||||
GpuImageDispatch::operator bool() const {
|
||||
const bool image_functions =
|
||||
uses_core ? create_image_core && destroy_image_core : create_image_khr && destroy_image_khr;
|
||||
return image_functions && image_target_texture;
|
||||
}
|
||||
|
||||
bool ValidateGpuBootstrapProbe(const GpuBootstrapProbe& probe, std::string* error) {
|
||||
const bool egl15 = AtLeastEgl15(probe);
|
||||
if (!egl15 && !HasExtension(probe.egl_extensions, "EGL_KHR_surfaceless_context")) {
|
||||
return Fail(error, "EGL surfaceless contexts are unavailable");
|
||||
}
|
||||
|
||||
const bool core_images = egl15 && probe.create_image_core && probe.destroy_image_core;
|
||||
const bool has_khr_image_extension =
|
||||
HasExtension(probe.egl_extensions, "EGL_KHR_image") || HasExtension(probe.egl_extensions, "EGL_KHR_image_base");
|
||||
const bool khr_images = has_khr_image_extension && probe.create_image_khr && probe.destroy_image_khr;
|
||||
if (!core_images && !khr_images) {
|
||||
return Fail(error, "EGL image creation is unavailable");
|
||||
}
|
||||
if (!HasExtension(probe.gl_extensions, "GL_OES_EGL_image")) {
|
||||
return Fail(error, "OpenGL EGL image binding is unavailable");
|
||||
}
|
||||
if (!probe.image_target_texture) {
|
||||
return Fail(error, "OpenGL EGL image entry point is unavailable");
|
||||
}
|
||||
if (error) error->clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ResolveGpuImageDispatch(EGLDisplay display, GpuImageDispatch* dispatch, std::string* error) {
|
||||
if (!dispatch || display == EGL_NO_DISPLAY || eglGetCurrentContext() == EGL_NO_CONTEXT) {
|
||||
return Fail(error, "No current EGL context is available");
|
||||
}
|
||||
|
||||
GpuBootstrapProbe probe;
|
||||
if (!ParseEglVersion(eglQueryString(display, EGL_VERSION), &probe.egl_major, &probe.egl_minor)) {
|
||||
return Fail(error, "EGL version is unavailable");
|
||||
}
|
||||
probe.egl_extensions = eglQueryString(display, EGL_EXTENSIONS);
|
||||
probe.gl_extensions = reinterpret_cast<const char*>(glGetString(GL_EXTENSIONS));
|
||||
probe.create_image_core = reinterpret_cast<void*>(eglGetProcAddress("eglCreateImage"));
|
||||
probe.destroy_image_core = reinterpret_cast<void*>(eglGetProcAddress("eglDestroyImage"));
|
||||
probe.create_image_khr = reinterpret_cast<void*>(eglGetProcAddress("eglCreateImageKHR"));
|
||||
probe.destroy_image_khr = reinterpret_cast<void*>(eglGetProcAddress("eglDestroyImageKHR"));
|
||||
probe.image_target_texture = reinterpret_cast<void*>(eglGetProcAddress("glEGLImageTargetTexture2DOES"));
|
||||
if (!ValidateGpuBootstrapProbe(probe, error)) return false;
|
||||
|
||||
GpuImageDispatch resolved;
|
||||
const bool egl15 = AtLeastEgl15(probe);
|
||||
if (egl15 && probe.create_image_core && probe.destroy_image_core) {
|
||||
resolved.uses_core = true;
|
||||
resolved.create_image_core = reinterpret_cast<EglCreateImageCoreProc>(probe.create_image_core);
|
||||
resolved.destroy_image_core = reinterpret_cast<EglDestroyImageCoreProc>(probe.destroy_image_core);
|
||||
} else {
|
||||
resolved.create_image_khr = reinterpret_cast<EglCreateImageKhrProc>(probe.create_image_khr);
|
||||
resolved.destroy_image_khr = reinterpret_cast<EglDestroyImageKhrProc>(probe.destroy_image_khr);
|
||||
}
|
||||
resolved.image_target_texture = reinterpret_cast<GlImageTargetTextureProc>(probe.image_target_texture);
|
||||
if (!resolved) return Fail(error, "GPU image dispatch is incomplete");
|
||||
*dispatch = resolved;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace mpv
|
||||
@@ -0,0 +1,47 @@
|
||||
#ifndef MPV_GPU_BOOTSTRAP_H_
|
||||
#define MPV_GPU_BOOTSTRAP_H_
|
||||
|
||||
#include <epoxy/egl.h>
|
||||
#include <epoxy/gl.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace mpv {
|
||||
|
||||
using EglCreateImageCoreProc = EGLImage (*)(EGLDisplay, EGLContext, EGLenum, EGLClientBuffer, const EGLAttrib*);
|
||||
using EglDestroyImageCoreProc = EGLBoolean (*)(EGLDisplay, EGLImage);
|
||||
using EglCreateImageKhrProc = EGLImageKHR (*)(EGLDisplay, EGLContext, EGLenum, EGLClientBuffer, const EGLint*);
|
||||
using EglDestroyImageKhrProc = EGLBoolean (*)(EGLDisplay, EGLImageKHR);
|
||||
using GlImageTargetTextureProc = void (*)(GLenum, GLeglImageOES);
|
||||
|
||||
struct GpuBootstrapProbe {
|
||||
int egl_major = 0;
|
||||
int egl_minor = 0;
|
||||
const char* egl_extensions = nullptr;
|
||||
const char* gl_extensions = nullptr;
|
||||
void* create_image_core = nullptr;
|
||||
void* destroy_image_core = nullptr;
|
||||
void* create_image_khr = nullptr;
|
||||
void* destroy_image_khr = nullptr;
|
||||
void* image_target_texture = nullptr;
|
||||
};
|
||||
|
||||
struct GpuImageDispatch {
|
||||
bool uses_core = false;
|
||||
EglCreateImageCoreProc create_image_core = nullptr;
|
||||
EglDestroyImageCoreProc destroy_image_core = nullptr;
|
||||
EglCreateImageKhrProc create_image_khr = nullptr;
|
||||
EglDestroyImageKhrProc destroy_image_khr = nullptr;
|
||||
GlImageTargetTextureProc image_target_texture = nullptr;
|
||||
|
||||
EGLImageKHR Create(EGLDisplay display, EGLContext context, EGLClientBuffer buffer) const;
|
||||
bool Destroy(EGLDisplay display, EGLImageKHR image) const;
|
||||
explicit operator bool() const;
|
||||
};
|
||||
|
||||
bool ValidateGpuBootstrapProbe(const GpuBootstrapProbe& probe, std::string* error);
|
||||
bool ResolveGpuImageDispatch(EGLDisplay display, GpuImageDispatch* dispatch, std::string* error);
|
||||
|
||||
} // namespace mpv
|
||||
|
||||
#endif // MPV_GPU_BOOTSTRAP_H_
|
||||
@@ -8,6 +8,9 @@
|
||||
#ifdef GDK_WINDOWING_WAYLAND
|
||||
#include <gdk/gdkwayland.h>
|
||||
#endif
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
#include <gdk/gdkx.h>
|
||||
#endif
|
||||
#include <locale.h>
|
||||
|
||||
// EGL 1.5 names; EGL_KHR_create_context introduced the same values earlier.
|
||||
@@ -589,6 +592,190 @@ bool MpvPlayer::InitRenderContextForSurface(EGLDisplay display, EGLConfig config
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MpvPlayer::InitRenderContext() {
|
||||
RetryPendingNativeTeardown();
|
||||
|
||||
std::lock_guard<std::mutex> lock(native_mutex_);
|
||||
if (audio_only_ || disposed_) {
|
||||
g_warning("MPV: Render context requested for an unavailable player");
|
||||
return false;
|
||||
}
|
||||
if (mpv_gl_) return true;
|
||||
if (!mpv_) {
|
||||
g_warning("MPV: Cannot create render context - mpv not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
// The texture path rides Flutter's EGL display and derives an isolated
|
||||
// context from Flutter's config, exactly as 2.11.0 did: the mpv FBO is
|
||||
// sampled by Flutter via an EGL image, so both sides must speak the same
|
||||
// display's formats. The caller (FlTextureGL::populate) has Flutter's
|
||||
// context current; capture it and restore it around every GL/EGL step
|
||||
// below, since mpv's render context creation may bind its own.
|
||||
const EGLDisplay flutter_display = eglGetCurrentDisplay();
|
||||
const EGLContext flutter_context = eglGetCurrentContext();
|
||||
const EGLSurface flutter_draw = eglGetCurrentSurface(EGL_DRAW);
|
||||
const EGLSurface flutter_read = eglGetCurrentSurface(EGL_READ);
|
||||
const EGLenum previous_api = eglQueryAPI();
|
||||
if (flutter_display == EGL_NO_DISPLAY || flutter_context == EGL_NO_CONTEXT || previous_api == EGL_NONE) {
|
||||
g_warning("MPV: No Flutter EGL context available for the texture render path");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto restore_flutter = [&]() {
|
||||
const EGLBoolean api_restored = previous_api == EGL_NONE ? EGL_TRUE : eglBindAPI(previous_api);
|
||||
const EGLBoolean restored =
|
||||
api_restored == EGL_TRUE ? eglMakeCurrent(flutter_display, flutter_draw, flutter_read, flutter_context)
|
||||
: EGL_FALSE;
|
||||
return restored == EGL_TRUE && api_restored == EGL_TRUE;
|
||||
};
|
||||
|
||||
EGLint config_id = 0;
|
||||
if (!eglQueryContext(flutter_display, flutter_context, EGL_CONFIG_ID, &config_id)) {
|
||||
g_warning("MPV: Failed to query Flutter EGL config: 0x%x", eglGetError());
|
||||
return false;
|
||||
}
|
||||
EGLConfig config = nullptr;
|
||||
EGLint num_configs = 0;
|
||||
const EGLint config_attribs[] = {EGL_CONFIG_ID, config_id, EGL_NONE};
|
||||
if (!eglChooseConfig(flutter_display, config_attribs, &config, 1, &num_configs) || num_configs != 1) {
|
||||
g_warning("MPV: Failed to select Flutter EGL config: 0x%x", eglGetError());
|
||||
return false;
|
||||
}
|
||||
if (!eglBindAPI(EGL_OPENGL_ES_API)) {
|
||||
g_warning("MPV: Failed to bind OpenGL ES API: 0x%x", eglGetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
// ES 2.0, as 2.11.0 shipped: this path exists to reproduce a configuration
|
||||
// hardware decode demonstrably worked against, not to push the driver.
|
||||
const EGLint context_attribs[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
|
||||
EGLContext candidate_context = eglCreateContext(flutter_display, config, EGL_NO_CONTEXT, context_attribs);
|
||||
if (candidate_context == EGL_NO_CONTEXT) {
|
||||
g_warning("MPV: Failed to create isolated EGL context: 0x%x", eglGetError());
|
||||
if (previous_api != EGL_NONE && !eglBindAPI(previous_api)) {
|
||||
g_warning("MPV: Failed to restore EGL client API: 0x%x", eglGetError());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
auto destroy_candidate_context = [&]() {
|
||||
const EGLenum api_before_cleanup = eglQueryAPI();
|
||||
if (eglGetCurrentContext() == candidate_context) {
|
||||
if (!eglBindAPI(EGL_OPENGL_ES_API) ||
|
||||
!eglMakeCurrent(flutter_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)) {
|
||||
g_warning("MPV: Failed to release rejected EGL context: 0x%x", eglGetError());
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!eglDestroyContext(flutter_display, candidate_context)) {
|
||||
g_warning("MPV: Failed to destroy rejected EGL context: 0x%x", eglGetError());
|
||||
}
|
||||
if (api_before_cleanup != EGL_NONE && !eglBindAPI(api_before_cleanup)) {
|
||||
g_warning("MPV: Failed to restore EGL API after context cleanup: 0x%x", eglGetError());
|
||||
}
|
||||
};
|
||||
|
||||
if (!eglMakeCurrent(flutter_display, EGL_NO_SURFACE, EGL_NO_SURFACE, candidate_context)) {
|
||||
g_warning("MPV: Failed to activate isolated EGL context: 0x%x", eglGetError());
|
||||
destroy_candidate_context();
|
||||
if (previous_api != EGL_NONE && !eglBindAPI(previous_api)) {
|
||||
g_warning("MPV: Failed to restore EGL client API: 0x%x", eglGetError());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
mpv_opengl_init_params gl_init_params{};
|
||||
gl_init_params.get_proc_address = get_opengl_proc_address;
|
||||
gl_init_params.get_proc_address_ctx = nullptr;
|
||||
mpv_render_param params[] = {
|
||||
{MPV_RENDER_PARAM_API_TYPE, const_cast<char*>(MPV_RENDER_API_TYPE_OPENGL)},
|
||||
{MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &gl_init_params},
|
||||
{MPV_RENDER_PARAM_INVALID, nullptr},
|
||||
{MPV_RENDER_PARAM_INVALID, nullptr},
|
||||
};
|
||||
|
||||
// The display handle for hwdec interop, whichever session this is: the
|
||||
// texture path is display-agnostic and must work under both X11 and
|
||||
// Wayland, so both slots are offered exactly as 2.11.0 did.
|
||||
GdkDisplay* gdk_display = gdk_display_get_default();
|
||||
#ifdef GDK_WINDOWING_WAYLAND
|
||||
if (gdk_display != nullptr && GDK_IS_WAYLAND_DISPLAY(gdk_display)) {
|
||||
params[2].type = MPV_RENDER_PARAM_WL_DISPLAY;
|
||||
params[2].data = gdk_wayland_display_get_wl_display(gdk_display);
|
||||
}
|
||||
#endif
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
if (gdk_display != nullptr && GDK_IS_X11_DISPLAY(gdk_display)) {
|
||||
params[2].type = MPV_RENDER_PARAM_X11_DISPLAY;
|
||||
params[2].data = gdk_x11_display_get_xdisplay(gdk_display);
|
||||
}
|
||||
#endif
|
||||
|
||||
mpv_render_context* candidate_gl = nullptr;
|
||||
const int error = mpv_render_context_create(&candidate_gl, mpv_, params);
|
||||
const bool restored = restore_flutter();
|
||||
if (error < 0 || candidate_gl == nullptr || !restored) {
|
||||
if (error < 0) {
|
||||
g_warning("MPV: mpv_render_context_create() failed for the texture path: %s", mpv_error_string(error));
|
||||
} else if (!restored) {
|
||||
g_warning("MPV: Failed to restore Flutter EGL state: 0x%x", eglGetError());
|
||||
} else {
|
||||
g_warning("MPV: mpv returned a null render context for the texture path");
|
||||
}
|
||||
if (candidate_gl) mpv_render_context_free(candidate_gl);
|
||||
destroy_candidate_context();
|
||||
return false;
|
||||
}
|
||||
|
||||
egl_display_ = flutter_display;
|
||||
egl_context_ = candidate_context;
|
||||
// The texture path renders into an 8-bit RGBA FBO; tell mpv the depth is 8
|
||||
// so it dithers for the format it actually draws into.
|
||||
surface_depth_bits_ = 8;
|
||||
mpv_gl_ = candidate_gl;
|
||||
mpv_render_context_set_update_callback(mpv_gl_, OnMpvRenderUpdate, callback_context_.get());
|
||||
g_message("MPV: Render context created on the Flutter-texture path");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MpvPlayer::HasRenderContext() const {
|
||||
std::lock_guard<std::mutex> lock(native_mutex_);
|
||||
return mpv_gl_ != nullptr;
|
||||
}
|
||||
|
||||
EGLDisplay MpvPlayer::GetEglDisplay() const {
|
||||
std::lock_guard<std::mutex> lock(native_mutex_);
|
||||
return egl_display_;
|
||||
}
|
||||
|
||||
EGLContext MpvPlayer::GetEglContext() const {
|
||||
std::lock_guard<std::mutex> lock(native_mutex_);
|
||||
return egl_context_;
|
||||
}
|
||||
|
||||
void MpvPlayer::Render(int width, int height, int fbo) {
|
||||
std::lock_guard<std::mutex> lock(native_mutex_);
|
||||
if (disposed_ || !mpv_gl_) return;
|
||||
|
||||
mpv_opengl_fbo mpv_fbo{};
|
||||
mpv_fbo.fbo = fbo;
|
||||
mpv_fbo.w = width;
|
||||
mpv_fbo.h = height;
|
||||
// 2.11.0 shipped this path with internal_format 0 and no flip: the FBO is
|
||||
// sampled by Flutter as a GL texture, and the orientation was correct with
|
||||
// these exact values. Keep them.
|
||||
mpv_fbo.internal_format = 0;
|
||||
|
||||
int flip_y = 0;
|
||||
mpv_render_param params[] = {
|
||||
{MPV_RENDER_PARAM_OPENGL_FBO, &mpv_fbo},
|
||||
{MPV_RENDER_PARAM_FLIP_Y, &flip_y},
|
||||
{MPV_RENDER_PARAM_INVALID, nullptr},
|
||||
};
|
||||
mpv_render_context_render(mpv_gl_, params);
|
||||
}
|
||||
|
||||
bool MpvPlayer::RenderToSurface(EGLSurface surface, int width, int height) {
|
||||
std::lock_guard<std::mutex> lock(native_mutex_);
|
||||
if (disposed_ || !mpv_gl_ || egl_context_ == EGL_NO_CONTEXT || surface == EGL_NO_SURFACE) return false;
|
||||
|
||||
@@ -98,6 +98,36 @@ class MpvPlayer {
|
||||
/// @return true if render context creation succeeded.
|
||||
bool InitRenderContextForSurface(EGLDisplay display, EGLConfig config, EGLSurface surface, int depth_bits);
|
||||
|
||||
/// Creates the mpv render context on an isolated ES 2.0 context derived
|
||||
/// from Flutter's current EGL display and config — the 2.11.0 texture path,
|
||||
/// restored as the SDR fallback for sessions that cannot host the Wayland
|
||||
/// plane (X11, XWayland, or a plane that failed to initialize). Shares
|
||||
/// nothing with Flutter's own context beyond the display (the isolated-
|
||||
/// context design from 3d9c6b20), and must be called while Flutter's EGL
|
||||
/// context is current — the texture's populate path is what guarantees
|
||||
/// that. The texture mode also passes the X11 display handle where present,
|
||||
/// which is what makes hwdec interop work under XWayland exactly as it did
|
||||
/// in 2.11.0. No-op once a render context exists in either mode.
|
||||
/// @return true if render context creation succeeded.
|
||||
bool InitRenderContext();
|
||||
|
||||
/// Whether a render context exists (either mode).
|
||||
bool HasRenderContext() const;
|
||||
|
||||
/// The EGL display/context backing the render context, for callers that
|
||||
/// need to activate it (the texture path renders into an offscreen FBO
|
||||
/// created in this context).
|
||||
EGLDisplay GetEglDisplay() const;
|
||||
EGLContext GetEglContext() const;
|
||||
|
||||
/// Renders one frame into the caller's FBO (texture mode). The caller owns
|
||||
/// the current EGL context and FBO binding; this mirrors the 2.11.0
|
||||
/// contract exactly — it does not make the context current and does not
|
||||
/// consume the redraw latch (MpvTexturePopulate clears the latch first, and
|
||||
/// the latch is what keeps Flutter's populate from being invoked
|
||||
/// needlessly).
|
||||
void Render(int width, int height, int fbo);
|
||||
|
||||
/// Renders one frame into |surface|'s default framebuffer. The caller
|
||||
/// presents it (eglSwapBuffers) once this returns.
|
||||
/// @return true if the frame was rendered.
|
||||
|
||||
+249
-17
@@ -7,11 +7,17 @@
|
||||
#include <new>
|
||||
#include <optional>
|
||||
|
||||
#include "mpv_texture.h"
|
||||
#include "wayland_video_surface.h"
|
||||
|
||||
using PlayerPtr = std::unique_ptr<mpv::MpvPlayer>;
|
||||
using VideoSurfacePtr = std::unique_ptr<mpv::WaylandVideoSurface>;
|
||||
|
||||
// Texture-bootstrap state machine (the SDR fallback path). kIdle means no
|
||||
// texture session exists; kPending means a texture is registered and Flutter
|
||||
// has been asked to populate it; kReady/kFailed are terminal for the session.
|
||||
enum class VideoBootstrapState { kIdle, kPending, kReady, kFailed };
|
||||
|
||||
// One queued HDR transaction: what to apply, and who to tell when it settles.
|
||||
//
|
||||
// `mode` is engaged only when the request *is* the mode change. Everything else
|
||||
@@ -36,9 +42,24 @@ struct _MpvPlugin {
|
||||
|
||||
PlayerPtr player;
|
||||
// The native Wayland plane every video instance renders into. Non-null once
|
||||
// initialize() has brought it up; there is no second render path, so a null
|
||||
// here on a video instance means initialization refused.
|
||||
// initialize() has brought it up. Where the plane cannot be brought up -
|
||||
// X11/XWayland sessions, a failed EGL bootstrap - the Flutter-texture path
|
||||
// (2.11.0's renderer) takes over as the SDR fallback, and a null plane with
|
||||
// a non-null texture means texture mode.
|
||||
VideoSurfacePtr video_surface;
|
||||
FlTextureRegistrar* texture_registrar;
|
||||
// The fallback render path: an FlTextureGL whose populate renders mpv into
|
||||
// an offscreen FBO sampled by Flutter. Owned via GObject ref; the plugin
|
||||
// keeps the player alive for it. Non-null only when a texture session has
|
||||
// been bootstrapped.
|
||||
MpvTexture* texture; // owned via GObject ref
|
||||
gboolean texture_registered;
|
||||
VideoBootstrapState bootstrap_state;
|
||||
gchar* bootstrap_error;
|
||||
// The one waitForVideoReady method call outstanding, answered when the
|
||||
// texture's GPU bootstrap settles or the 5 s deadline expires.
|
||||
FlMethodCall* ready_call;
|
||||
guint ready_timeout_source_id;
|
||||
// Set when the plane must be redrawn even though mpv has no new frame -
|
||||
// after a resize or after becoming visible, where the buffer on screen is
|
||||
// stale or absent. Sticky, because the render it asks for may first have to
|
||||
@@ -192,6 +213,97 @@ static void send_named_event(MpvPlugin* self, const char* name) {
|
||||
send_event(self, event);
|
||||
}
|
||||
|
||||
static gboolean handle_ready_timeout(gpointer user_data);
|
||||
static void complete_ready_call(MpvPlugin* self, gboolean success, const char* message) {
|
||||
if (self->ready_timeout_source_id != 0) {
|
||||
g_source_remove(self->ready_timeout_source_id);
|
||||
self->ready_timeout_source_id = 0;
|
||||
}
|
||||
if (!self->ready_call) return;
|
||||
g_autoptr(FlMethodResponse) response =
|
||||
success ? FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr))
|
||||
: FL_METHOD_RESPONSE(fl_method_error_response_new(
|
||||
"INIT_FAILED", message ? message : "Video initialization failed", nullptr));
|
||||
fl_method_call_respond(self->ready_call, response, nullptr);
|
||||
g_object_unref(self->ready_call);
|
||||
self->ready_call = nullptr;
|
||||
}
|
||||
|
||||
// The texture's populate callback runs on Flutter's raster thread, so the
|
||||
// outcome is bounced to the GTK main context before touching the plugin's
|
||||
// state.
|
||||
struct TextureReadyContext {
|
||||
MpvPlugin* plugin; // g_object_ref'd
|
||||
guint64 generation;
|
||||
};
|
||||
|
||||
struct TextureReadyResult {
|
||||
MpvPlugin* plugin; // owns a ref
|
||||
guint64 generation;
|
||||
gboolean success;
|
||||
gchar* message;
|
||||
};
|
||||
|
||||
static gboolean handle_texture_ready_result(gpointer data) {
|
||||
auto* result = static_cast<TextureReadyResult*>(data);
|
||||
MpvPlugin* self = result->plugin;
|
||||
if (result->generation != self->generation || self->bootstrap_state != VideoBootstrapState::kPending) {
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
if (result->success) {
|
||||
self->bootstrap_state = VideoBootstrapState::kReady;
|
||||
self->initialized = TRUE;
|
||||
complete_ready_call(self, TRUE, nullptr);
|
||||
} else {
|
||||
self->bootstrap_state = VideoBootstrapState::kFailed;
|
||||
g_free(self->bootstrap_error);
|
||||
self->bootstrap_error = g_strdup(result->message ? result->message : "Video initialization failed");
|
||||
complete_ready_call(self, FALSE, self->bootstrap_error);
|
||||
release_video_resources(self);
|
||||
}
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static void destroy_texture_ready_result(gpointer data) {
|
||||
auto* result = static_cast<TextureReadyResult*>(data);
|
||||
g_object_unref(result->plugin);
|
||||
g_free(result->message);
|
||||
delete result;
|
||||
}
|
||||
|
||||
static void on_texture_ready(gboolean success, const gchar* message, gpointer user_data) {
|
||||
auto* context = static_cast<TextureReadyContext*>(user_data);
|
||||
auto* result = new TextureReadyResult{
|
||||
MPV_PLUGIN(g_object_ref(context->plugin)),
|
||||
context->generation,
|
||||
success,
|
||||
g_strdup(message),
|
||||
};
|
||||
g_main_context_invoke_full(
|
||||
nullptr, G_PRIORITY_DEFAULT, handle_texture_ready_result, result, destroy_texture_ready_result);
|
||||
}
|
||||
|
||||
static void destroy_texture_ready_context(gpointer data) {
|
||||
auto* context = static_cast<TextureReadyContext*>(data);
|
||||
g_object_unref(context->plugin);
|
||||
delete context;
|
||||
}
|
||||
|
||||
static gboolean handle_ready_timeout(gpointer user_data) {
|
||||
MpvPlugin* self = MPV_PLUGIN(user_data);
|
||||
self->ready_timeout_source_id = 0;
|
||||
if (self->bootstrap_state != VideoBootstrapState::kPending || !self->ready_call) {
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
self->bootstrap_state = VideoBootstrapState::kFailed;
|
||||
g_free(self->bootstrap_error);
|
||||
self->bootstrap_error = g_strdup("Video texture did not become ready before the initialization deadline");
|
||||
complete_ready_call(self, FALSE, self->bootstrap_error);
|
||||
release_video_resources(self);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static void release_video_resources(MpvPlugin* self) {
|
||||
// Anything still in flight is now answering for a plane that is going away.
|
||||
++self->generation;
|
||||
@@ -201,6 +313,7 @@ static void release_video_resources(MpvPlugin* self) {
|
||||
g_source_remove(self->hdr_mpv_leg_timeout_source_);
|
||||
self->hdr_mpv_leg_timeout_source_ = 0;
|
||||
}
|
||||
complete_ready_call(self, FALSE, "Video initialization was cancelled");
|
||||
// Queued transactions will never run, and each may be holding a reference to a
|
||||
// Dart method call that has to be answered or it is leaked along with its
|
||||
// response.
|
||||
@@ -212,12 +325,25 @@ static void release_video_resources(MpvPlugin* self) {
|
||||
if (request.done) request.done(MPV_ERROR_UNINITIALIZED);
|
||||
}
|
||||
if (self->player) {
|
||||
// The plane is a raw callback target. Revoke every callback path before
|
||||
// tearing it down; Dispose then drains any callback already holding a
|
||||
// native lease.
|
||||
// The plane and the texture are raw callback targets. Revoke every
|
||||
// callback path before tearing either down; Dispose then drains any
|
||||
// callback already holding a native lease.
|
||||
self->player->SetRedrawCallback(nullptr);
|
||||
self->player->SetSourceMetadataCallback(nullptr);
|
||||
self->player->SetEventCallback(nullptr);
|
||||
}
|
||||
if (self->texture) {
|
||||
// Texture must be disposed BEFORE the player — mpv_texture_dispose needs
|
||||
// the player's EGL context to clean up GL resources.
|
||||
if (self->texture_registered && self->texture_registrar) {
|
||||
fl_texture_registrar_unregister_texture(self->texture_registrar, FL_TEXTURE(self->texture));
|
||||
self->texture_registered = FALSE;
|
||||
}
|
||||
mpv_texture_dispose(self->texture);
|
||||
g_object_unref(self->texture);
|
||||
self->texture = nullptr;
|
||||
}
|
||||
if (self->player) {
|
||||
self->player->Dispose();
|
||||
self->player.reset();
|
||||
}
|
||||
@@ -231,6 +357,8 @@ static void release_video_resources(MpvPlugin* self) {
|
||||
self->initialized = FALSE;
|
||||
self->visible = FALSE;
|
||||
self->plane_needs_render = FALSE;
|
||||
self->bootstrap_state = VideoBootstrapState::kIdle;
|
||||
g_clear_pointer(&self->bootstrap_error, g_free);
|
||||
// All of these describe a plane and an mpv instance that no longer exist, and
|
||||
// initialize() builds both fresh: a new MpvPlayer whose applied target
|
||||
// properties are back to "auto", and a new surface with no description
|
||||
@@ -887,6 +1015,13 @@ static void mpv_plugin_init(MpvPlugin* self) {
|
||||
self->initialized = FALSE;
|
||||
self->audio_only = FALSE;
|
||||
self->generation = 0;
|
||||
self->texture = nullptr;
|
||||
self->texture_registered = FALSE;
|
||||
self->texture_registrar = nullptr;
|
||||
self->bootstrap_state = VideoBootstrapState::kIdle;
|
||||
self->bootstrap_error = nullptr;
|
||||
self->ready_call = nullptr;
|
||||
self->ready_timeout_source_id = 0;
|
||||
}
|
||||
|
||||
MpvPlugin* mpv_plugin_new(FlPluginRegistrar* registrar, const gchar* channel_name, gboolean audio_only) {
|
||||
@@ -894,6 +1029,9 @@ MpvPlugin* mpv_plugin_new(FlPluginRegistrar* registrar, const gchar* channel_nam
|
||||
|
||||
self->registrar = FL_PLUGIN_REGISTRAR(g_object_ref(registrar));
|
||||
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);
|
||||
|
||||
@@ -952,7 +1090,18 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel, FlMethodCall
|
||||
}
|
||||
} else if (self->video_surface && self->video_surface->valid()) {
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(fl_value_new_bool(TRUE)));
|
||||
} else if (self->texture && (self->bootstrap_state == VideoBootstrapState::kPending ||
|
||||
self->bootstrap_state == VideoBootstrapState::kReady)) {
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(fl_value_new_int(mpv_texture_get_id(self->texture))));
|
||||
} else {
|
||||
// Optional 'renderMode' argument: "texture" forces the SDR
|
||||
// Flutter-texture fallback (the user-visible workaround for plane-only
|
||||
// trouble); anything else prefers the Wayland plane and falls back only
|
||||
// when it cannot be brought up.
|
||||
FlValue* render_mode_value = args != nullptr ? fl_value_lookup_string(args, "renderMode") : nullptr;
|
||||
const bool force_texture =
|
||||
render_mode_value != nullptr && fl_value_get_type(render_mode_value) == FL_VALUE_TYPE_STRING &&
|
||||
g_strcmp0(fl_value_get_string(render_mode_value), "texture") == 0;
|
||||
if (!self->player || self->player->IsDisposed()) {
|
||||
self->player = std::make_unique<mpv::MpvPlayer>();
|
||||
}
|
||||
@@ -962,22 +1111,86 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel, FlMethodCall
|
||||
release_video_resources(self);
|
||||
response =
|
||||
FL_METHOD_RESPONSE(fl_method_error_response_new("INIT_FAILED", "Failed to initialize MPV player", nullptr));
|
||||
} else if (start_video_plane(self, fl_plugin_registrar_get_view(self->registrar), &error)) {
|
||||
++self->generation;
|
||||
self->player->SetEventCallback([self](FlValue* event) { send_event(self, event); });
|
||||
self->initialized = TRUE;
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(fl_value_new_bool(TRUE)));
|
||||
} else {
|
||||
// There is no second render path. Refuse with the reason rather than
|
||||
// presenting into something the user cannot see.
|
||||
g_warning("MPV: no video plane: %s", error.c_str());
|
||||
release_video_resources(self);
|
||||
response = FL_METHOD_RESPONSE(fl_method_error_response_new("VIDEO_PLANE_UNSUPPORTED", error.c_str(), nullptr));
|
||||
bool plane_up = false;
|
||||
if (!force_texture) {
|
||||
plane_up = start_video_plane(self, fl_plugin_registrar_get_view(self->registrar), &error);
|
||||
}
|
||||
if (plane_up) {
|
||||
++self->generation;
|
||||
self->player->SetEventCallback([self](FlValue* event) { send_event(self, event); });
|
||||
self->initialized = TRUE;
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(fl_value_new_bool(TRUE)));
|
||||
} else if (self->texture_registrar != nullptr) {
|
||||
// The plane cannot host this session (X11/XWayland, or the plane
|
||||
// bootstrap failed). Fall back to the 2.11.0 Flutter-texture path:
|
||||
// display-agnostic, SDR only, and the exact environment hardware
|
||||
// decode demonstrably worked in. The texture is registered
|
||||
// immediately so Flutter can invoke FlTextureGL::populate; playback
|
||||
// stays gated until the GPU bootstrap reports the texture usable
|
||||
// (the Dart side awaits waitForVideoReady).
|
||||
if (!force_texture) {
|
||||
g_warning("MPV: no video plane (%s); falling back to the Flutter-texture path", error.c_str());
|
||||
}
|
||||
FlView* view = fl_plugin_registrar_get_view(self->registrar);
|
||||
self->texture = mpv_texture_new(self->player.get(), self->texture_registrar, view);
|
||||
++self->generation;
|
||||
self->bootstrap_state = VideoBootstrapState::kPending;
|
||||
auto* ready_context = new TextureReadyContext{MPV_PLUGIN(g_object_ref(self)), self->generation};
|
||||
mpv_texture_set_ready_callback(self->texture, on_texture_ready, ready_context, destroy_texture_ready_context);
|
||||
|
||||
if (!fl_texture_registrar_register_texture(self->texture_registrar, FL_TEXTURE(self->texture))) {
|
||||
self->bootstrap_state = VideoBootstrapState::kFailed;
|
||||
self->bootstrap_error = g_strdup("Failed to register video texture");
|
||||
release_video_resources(self);
|
||||
response =
|
||||
FL_METHOD_RESPONSE(fl_method_error_response_new("INIT_FAILED", self->bootstrap_error, nullptr));
|
||||
} else {
|
||||
self->texture_registered = TRUE;
|
||||
MpvTexture* texture = self->texture;
|
||||
self->player->SetRedrawCallback([texture]() { mpv_texture_mark_frame_available(texture); });
|
||||
self->player->SetEventCallback([self](FlValue* event) { send_event(self, event); });
|
||||
mpv_texture_mark_frame_available(self->texture);
|
||||
response = FL_METHOD_RESPONSE(
|
||||
fl_method_success_response_new(fl_value_new_int(mpv_texture_get_id(self->texture))));
|
||||
}
|
||||
} else {
|
||||
// No texture registrar at all: refuse with the plane's reason
|
||||
// rather than presenting into something the user cannot see.
|
||||
g_warning("MPV: no video plane: %s", error.c_str());
|
||||
release_video_resources(self);
|
||||
response = FL_METHOD_RESPONSE(fl_method_error_response_new("VIDEO_PLANE_UNSUPPORTED", error.c_str(), nullptr));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (strcmp(method, "dispose") == 0) {
|
||||
release_video_resources(self);
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||
} else if (strcmp(method, "waitForVideoReady") == 0) {
|
||||
// Texture-path only: initialize returns the texture id as soon as the
|
||||
// texture is registered, but the GPU bootstrap (FlTextureGL::populate,
|
||||
// which creates the render context and the shared EGL image) completes
|
||||
// asynchronously. Playback must not start against an unusable texture.
|
||||
if (self->audio_only) {
|
||||
response = FL_METHOD_RESPONSE(
|
||||
fl_method_error_response_new("INIT_FAILED", "Audio players have no video readiness state", nullptr));
|
||||
} else if (self->bootstrap_state == VideoBootstrapState::kReady && self->initialized) {
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||
} else if (self->bootstrap_state == VideoBootstrapState::kFailed) {
|
||||
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
|
||||
"INIT_FAILED", self->bootstrap_error ? self->bootstrap_error : "Video initialization failed", nullptr));
|
||||
} else if (self->bootstrap_state != VideoBootstrapState::kPending || !self->texture) {
|
||||
response = FL_METHOD_RESPONSE(
|
||||
fl_method_error_response_new("INIT_FAILED", "Video initialization is not pending", nullptr));
|
||||
} else if (self->ready_call) {
|
||||
response = FL_METHOD_RESPONSE(
|
||||
fl_method_error_response_new("INIT_IN_PROGRESS", "Video readiness is already being awaited", nullptr));
|
||||
} else {
|
||||
self->ready_call = FL_METHOD_CALL(g_object_ref(method_call));
|
||||
self->ready_timeout_source_id = g_timeout_add_seconds_full(
|
||||
G_PRIORITY_DEFAULT, 5, handle_ready_timeout, g_object_ref(self), g_object_unref);
|
||||
return;
|
||||
}
|
||||
} else if (strcmp(method, "command") == 0) {
|
||||
if (!self->player || !self->initialized) {
|
||||
response = FL_METHOD_RESPONSE(fl_method_error_response_new("NOT_INITIALIZED", "Player not initialized", nullptr));
|
||||
@@ -1133,6 +1346,17 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel, FlMethodCall
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else if (!self->video_surface && !self->audio_only && g_strcmp0(fl_value_get_string(name_value), "hdr-tone-mapping") == 0) {
|
||||
// Texture-fallback mode has no plane and no description machinery, so
|
||||
// the tone-map owner is meaningless. Answered success so the startup
|
||||
// preference push does not read a refusal.
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||
} else if (!self->video_surface && !self->audio_only && g_strcmp0(fl_value_get_string(name_value), "hdr-enabled") == 0) {
|
||||
// No plane, no HDR. Honest refusal; Dart swallows it on Linux and
|
||||
// reconciles the stored preference down, so the settings switch
|
||||
// agrees with the session.
|
||||
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
|
||||
"HDR_UNSUPPORTED", "This session has no video plane and cannot carry HDR", nullptr));
|
||||
} else if (!self->audio_only && g_strcmp0(fl_value_get_string(name_value), "vo") == 0 &&
|
||||
g_strcmp0(fl_value_get_string(value_value), "libmpv") != 0) {
|
||||
// Embedded rendering is authoritative: the render context was created
|
||||
@@ -1267,6 +1491,10 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel, FlMethodCall
|
||||
// consumed (or suppressed) while hidden, so no callback is pending.
|
||||
if (self->visible) render_video_plane(self, TRUE);
|
||||
}
|
||||
} else if (self->visible && self->texture) {
|
||||
// Texture path: becoming visible has to kick Flutter's populate, for
|
||||
// the same reason the plane renders explicitly above.
|
||||
mpv_texture_mark_frame_available(self->texture);
|
||||
}
|
||||
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||
@@ -1345,8 +1573,12 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel, FlMethodCall
|
||||
} else if (strcmp(method, "updateFrame") == 0) {
|
||||
// The cross-platform "kick the video output" call. On the plane that means
|
||||
// forcing a render: mpv may have no new frame, but the caller is asking
|
||||
// because what is on screen is stale.
|
||||
if (self->visible) render_video_plane(self, TRUE);
|
||||
// because what is on screen is stale. On the texture path the same kick
|
||||
// marks the texture available.
|
||||
if (self->visible) {
|
||||
if (self->video_surface) render_video_plane(self, TRUE);
|
||||
if (self->texture) mpv_texture_mark_frame_available(self->texture);
|
||||
}
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||
} else if (strcmp(method, "isInitialized") == 0) {
|
||||
gboolean initialized = self->player && self->initialized;
|
||||
|
||||
@@ -0,0 +1,540 @@
|
||||
#include "mpv_texture.h"
|
||||
|
||||
#include <epoxy/egl.h>
|
||||
#include <epoxy/gl.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "mpv_gpu_bootstrap.h"
|
||||
|
||||
namespace {
|
||||
|
||||
GQuark TextureErrorDomain() { return g_quark_from_static_string("plezy-mpv-texture"); }
|
||||
|
||||
struct TextureResources {
|
||||
GLuint mpv_fbo = 0;
|
||||
GLuint mpv_texture = 0;
|
||||
GLuint flutter_texture = 0;
|
||||
EGLImageKHR egl_image = EGL_NO_IMAGE_KHR;
|
||||
int32_t width = 0;
|
||||
int32_t height = 0;
|
||||
|
||||
bool complete() const {
|
||||
return mpv_fbo != 0 && mpv_texture != 0 && flutter_texture != 0 && egl_image != EGL_NO_IMAGE_KHR;
|
||||
}
|
||||
};
|
||||
|
||||
bool SetError(GError** error, const char* message) {
|
||||
g_set_error_literal(error, TextureErrorDomain(), 1, message);
|
||||
return false;
|
||||
}
|
||||
|
||||
void ClearGlErrors() {
|
||||
while (glGetError() != GL_NO_ERROR) {
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct _MpvTexture {
|
||||
FlTextureGL parent_instance;
|
||||
|
||||
mpv::MpvPlayer* player;
|
||||
FlTextureRegistrar* registrar;
|
||||
FlView* view;
|
||||
|
||||
GMutex mutex;
|
||||
bool disposed;
|
||||
TextureResources* active;
|
||||
std::vector<TextureResources>* retired;
|
||||
mpv::GpuImageDispatch* image_dispatch;
|
||||
EGLDisplay flutter_display;
|
||||
EGLContext flutter_share_context;
|
||||
EGLContext flutter_cleanup_context;
|
||||
|
||||
GMutex bootstrap_mutex;
|
||||
gint bootstrap_state;
|
||||
gchar* bootstrap_error;
|
||||
MpvTextureReadyCallback ready_callback;
|
||||
gpointer ready_user_data;
|
||||
GDestroyNotify ready_destroy_notify;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(MpvTexture, mpv_texture, fl_texture_gl_get_type())
|
||||
|
||||
namespace {
|
||||
|
||||
void SignalBootstrap(MpvTexture* self, gboolean success, const char* message) {
|
||||
MpvTextureReadyCallback callback = nullptr;
|
||||
gpointer user_data = nullptr;
|
||||
g_mutex_lock(&self->bootstrap_mutex);
|
||||
if (self->bootstrap_state == 0) {
|
||||
self->bootstrap_state = success ? 1 : 2;
|
||||
if (!success) self->bootstrap_error = g_strdup(message ? message : "Video initialization failed");
|
||||
callback = self->ready_callback;
|
||||
user_data = self->ready_user_data;
|
||||
}
|
||||
g_mutex_unlock(&self->bootstrap_mutex);
|
||||
if (callback) callback(success, message, user_data);
|
||||
}
|
||||
|
||||
bool RestoreContext(
|
||||
EGLDisplay display, EGLSurface draw, EGLSurface read, EGLContext context, EGLenum api, GError** error) {
|
||||
if (api != EGL_NONE && !eglBindAPI(api)) {
|
||||
g_warning("MPV texture: failed to restore Flutter EGL API: 0x%x", eglGetError());
|
||||
return SetError(error, "Failed to restore Flutter EGL API");
|
||||
}
|
||||
if (eglMakeCurrent(display, draw, read, context)) return true;
|
||||
g_warning("MPV texture: failed to restore EGL context: 0x%x", eglGetError());
|
||||
return SetError(error, "Failed to restore Flutter EGL context");
|
||||
}
|
||||
|
||||
void RestoreOrReleaseContext(
|
||||
EGLDisplay flutter_display, EGLSurface flutter_draw, EGLSurface flutter_read, EGLContext flutter_context,
|
||||
EGLenum flutter_api, EGLDisplay mpv_display) {
|
||||
if (flutter_display != EGL_NO_DISPLAY && flutter_context != EGL_NO_CONTEXT) {
|
||||
const bool api_restored = flutter_api == EGL_NONE || eglBindAPI(flutter_api);
|
||||
if (api_restored && eglMakeCurrent(flutter_display, flutter_draw, flutter_read, flutter_context)) return;
|
||||
g_warning("MPV texture: failed to restore EGL state during cleanup: 0x%x", eglGetError());
|
||||
}
|
||||
|
||||
if (mpv_display != EGL_NO_DISPLAY) {
|
||||
if (!eglBindAPI(EGL_OPENGL_ES_API)) {
|
||||
g_warning("MPV texture: failed to bind OpenGL while releasing cleanup context: 0x%x", eglGetError());
|
||||
} else if (!eglMakeCurrent(mpv_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)) {
|
||||
g_warning("MPV texture: failed to release EGL context during cleanup: 0x%x", eglGetError());
|
||||
}
|
||||
}
|
||||
if (flutter_api != EGL_NONE && !eglBindAPI(flutter_api)) {
|
||||
g_warning("MPV texture: failed to restore Flutter EGL API after cleanup: 0x%x", eglGetError());
|
||||
}
|
||||
}
|
||||
|
||||
bool ResourceSetEmpty(const TextureResources& resources) {
|
||||
return resources.mpv_fbo == 0 && resources.mpv_texture == 0 && resources.flutter_texture == 0 &&
|
||||
resources.egl_image == EGL_NO_IMAGE_KHR;
|
||||
}
|
||||
|
||||
bool EnsureFlutterCleanupContext(
|
||||
MpvTexture* self, EGLDisplay flutter_display, EGLContext flutter_context, EGLenum flutter_api, GError** error) {
|
||||
if (self->flutter_cleanup_context != EGL_NO_CONTEXT) {
|
||||
if (self->flutter_display == flutter_display && self->flutter_share_context == flutter_context) return true;
|
||||
return SetError(error, "Flutter EGL context changed while video textures were active");
|
||||
}
|
||||
if (flutter_api != EGL_OPENGL_ES_API) {
|
||||
return SetError(error, "Flutter is not using an OpenGL ES context");
|
||||
}
|
||||
|
||||
EGLint config_id = 0;
|
||||
EGLint client_version = 0;
|
||||
if (!eglQueryContext(flutter_display, flutter_context, EGL_CONFIG_ID, &config_id) ||
|
||||
!eglQueryContext(flutter_display, flutter_context, EGL_CONTEXT_CLIENT_VERSION, &client_version)) {
|
||||
g_warning("MPV texture: failed to query Flutter EGL context: 0x%x", eglGetError());
|
||||
return SetError(error, "Failed to query Flutter EGL context");
|
||||
}
|
||||
EGLConfig config = nullptr;
|
||||
EGLint num_configs = 0;
|
||||
const EGLint config_attribs[] = {EGL_CONFIG_ID, config_id, EGL_NONE};
|
||||
if (!eglChooseConfig(flutter_display, config_attribs, &config, 1, &num_configs) || num_configs != 1) {
|
||||
g_warning("MPV texture: failed to select Flutter EGL config: 0x%x", eglGetError());
|
||||
return SetError(error, "Failed to select Flutter EGL config");
|
||||
}
|
||||
|
||||
if (!eglBindAPI(EGL_OPENGL_ES_API)) {
|
||||
g_warning("MPV texture: failed to bind OpenGL ES for cleanup context creation: 0x%x", eglGetError());
|
||||
return SetError(error, "Failed to bind OpenGL ES for video cleanup");
|
||||
}
|
||||
const EGLint context_attribs[] = {EGL_CONTEXT_CLIENT_VERSION, client_version, EGL_NONE};
|
||||
const EGLContext cleanup_context = eglCreateContext(flutter_display, config, flutter_context, context_attribs);
|
||||
const bool api_restored = eglBindAPI(flutter_api) == EGL_TRUE;
|
||||
if (cleanup_context == EGL_NO_CONTEXT || !api_restored) {
|
||||
if (cleanup_context != EGL_NO_CONTEXT && !eglDestroyContext(flutter_display, cleanup_context)) {
|
||||
g_warning("MPV texture: failed to destroy rejected cleanup context: 0x%x", eglGetError());
|
||||
}
|
||||
if (!api_restored) {
|
||||
g_warning("MPV texture: failed to restore Flutter EGL API after cleanup context creation: 0x%x", eglGetError());
|
||||
}
|
||||
return SetError(error, "Failed to create video cleanup context");
|
||||
}
|
||||
|
||||
self->flutter_display = flutter_display;
|
||||
self->flutter_share_context = flutter_context;
|
||||
self->flutter_cleanup_context = cleanup_context;
|
||||
return true;
|
||||
}
|
||||
|
||||
void DestroyFlutterCleanupContext(MpvTexture* self) {
|
||||
if (self->flutter_cleanup_context == EGL_NO_CONTEXT || self->flutter_display == EGL_NO_DISPLAY) return;
|
||||
|
||||
const EGLenum previous_api = eglQueryAPI();
|
||||
if (eglGetCurrentContext() == self->flutter_cleanup_context) {
|
||||
if (!eglBindAPI(EGL_OPENGL_ES_API) ||
|
||||
!eglMakeCurrent(self->flutter_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)) {
|
||||
g_warning("MPV texture: failed to release Flutter cleanup context: 0x%x", eglGetError());
|
||||
}
|
||||
}
|
||||
if (!eglDestroyContext(self->flutter_display, self->flutter_cleanup_context)) {
|
||||
g_warning("MPV texture: failed to destroy Flutter cleanup context: 0x%x", eglGetError());
|
||||
}
|
||||
if (previous_api != EGL_NONE && !eglBindAPI(previous_api)) {
|
||||
g_warning("MPV texture: failed to restore EGL API after cleanup context destruction: 0x%x", eglGetError());
|
||||
}
|
||||
self->flutter_cleanup_context = EGL_NO_CONTEXT;
|
||||
self->flutter_share_context = EGL_NO_CONTEXT;
|
||||
self->flutter_display = EGL_NO_DISPLAY;
|
||||
}
|
||||
|
||||
void RetireIncompleteCandidate(MpvTexture* self, const TextureResources& candidate) {
|
||||
if (!ResourceSetEmpty(candidate)) self->retired->push_back(candidate);
|
||||
}
|
||||
|
||||
void CleanupResourceSet(
|
||||
MpvTexture* self, TextureResources* resources, EGLDisplay flutter_display, EGLSurface flutter_draw,
|
||||
EGLSurface flutter_read, EGLContext flutter_context, EGLenum flutter_api) {
|
||||
const EGLDisplay mpv_display = self->player ? self->player->GetEglDisplay() : EGL_NO_DISPLAY;
|
||||
const EGLContext mpv_context = self->player ? self->player->GetEglContext() : EGL_NO_CONTEXT;
|
||||
|
||||
if (resources->egl_image != EGL_NO_IMAGE_KHR && mpv_display != EGL_NO_DISPLAY && self->image_dispatch &&
|
||||
*self->image_dispatch) {
|
||||
if (self->image_dispatch->Destroy(mpv_display, resources->egl_image)) {
|
||||
resources->egl_image = EGL_NO_IMAGE_KHR;
|
||||
} else {
|
||||
g_warning("MPV texture: failed to destroy EGL image: 0x%x", eglGetError());
|
||||
}
|
||||
}
|
||||
|
||||
if (resources->flutter_texture) {
|
||||
const bool flutter_current = flutter_display == self->flutter_display &&
|
||||
flutter_context == self->flutter_share_context && flutter_context != EGL_NO_CONTEXT &&
|
||||
eglGetCurrentContext() == self->flutter_share_context;
|
||||
const bool cleanup_current =
|
||||
!flutter_current && self->flutter_display != EGL_NO_DISPLAY &&
|
||||
self->flutter_cleanup_context != EGL_NO_CONTEXT && eglBindAPI(EGL_OPENGL_ES_API) &&
|
||||
eglMakeCurrent(self->flutter_display, EGL_NO_SURFACE, EGL_NO_SURFACE, self->flutter_cleanup_context);
|
||||
if (flutter_current || cleanup_current) {
|
||||
glDeleteTextures(1, &resources->flutter_texture);
|
||||
resources->flutter_texture = 0;
|
||||
} else {
|
||||
g_warning("MPV texture: failed to activate Flutter cleanup context: 0x%x", eglGetError());
|
||||
}
|
||||
if (cleanup_current) {
|
||||
RestoreOrReleaseContext(
|
||||
flutter_display, flutter_draw, flutter_read, flutter_context, flutter_api, self->flutter_display);
|
||||
}
|
||||
}
|
||||
|
||||
if (resources->mpv_fbo || resources->mpv_texture) {
|
||||
const bool mpv_current = mpv_display != EGL_NO_DISPLAY && mpv_context != EGL_NO_CONTEXT &&
|
||||
eglBindAPI(EGL_OPENGL_ES_API) &&
|
||||
eglMakeCurrent(mpv_display, EGL_NO_SURFACE, EGL_NO_SURFACE, mpv_context);
|
||||
if (mpv_current) {
|
||||
if (resources->mpv_fbo) glDeleteFramebuffers(1, &resources->mpv_fbo);
|
||||
if (resources->mpv_texture) glDeleteTextures(1, &resources->mpv_texture);
|
||||
resources->mpv_fbo = 0;
|
||||
resources->mpv_texture = 0;
|
||||
} else {
|
||||
g_warning("MPV texture: failed to activate EGL context during cleanup: 0x%x", eglGetError());
|
||||
}
|
||||
RestoreOrReleaseContext(flutter_display, flutter_draw, flutter_read, flutter_context, flutter_api, mpv_display);
|
||||
}
|
||||
}
|
||||
|
||||
void CleanupRetired(MpvTexture* self) {
|
||||
if (!self->retired || self->retired->empty() || !self->player) return;
|
||||
const EGLDisplay flutter_display = eglGetCurrentDisplay();
|
||||
const EGLContext flutter_context = eglGetCurrentContext();
|
||||
const EGLSurface flutter_draw = eglGetCurrentSurface(EGL_DRAW);
|
||||
const EGLSurface flutter_read = eglGetCurrentSurface(EGL_READ);
|
||||
const EGLenum flutter_api = eglQueryAPI();
|
||||
for (auto& resources : *self->retired) {
|
||||
CleanupResourceSet(self, &resources, flutter_display, flutter_draw, flutter_read, flutter_context, flutter_api);
|
||||
}
|
||||
auto& retired = *self->retired;
|
||||
retired.erase(
|
||||
std::remove_if(
|
||||
retired.begin(), retired.end(),
|
||||
[](const TextureResources& resources) { return ResourceSetEmpty(resources); }),
|
||||
retired.end());
|
||||
}
|
||||
|
||||
bool EnsureTextures(MpvTexture* self, int32_t width, int32_t height, GError** error) {
|
||||
if (self->active->complete() && self->active->width == width && self->active->height == height) return true;
|
||||
|
||||
const EGLDisplay flutter_display = eglGetCurrentDisplay();
|
||||
const EGLContext flutter_context = eglGetCurrentContext();
|
||||
const EGLSurface flutter_draw = eglGetCurrentSurface(EGL_DRAW);
|
||||
const EGLSurface flutter_read = eglGetCurrentSurface(EGL_READ);
|
||||
const EGLenum flutter_api = eglQueryAPI();
|
||||
const EGLDisplay mpv_display = self->player->GetEglDisplay();
|
||||
const EGLContext mpv_context = self->player->GetEglContext();
|
||||
if (flutter_display == EGL_NO_DISPLAY || flutter_context == EGL_NO_CONTEXT || mpv_display == EGL_NO_DISPLAY ||
|
||||
mpv_context == EGL_NO_CONTEXT) {
|
||||
return SetError(error, "Video EGL contexts are unavailable");
|
||||
}
|
||||
|
||||
if (!EnsureFlutterCleanupContext(self, flutter_display, flutter_context, flutter_api, error)) return false;
|
||||
|
||||
if (!*self->image_dispatch) {
|
||||
std::string dispatch_error;
|
||||
if (!mpv::ResolveGpuImageDispatch(flutter_display, self->image_dispatch, &dispatch_error)) {
|
||||
g_warning("MPV texture: GPU bootstrap rejected: %s", dispatch_error.c_str());
|
||||
return SetError(error, dispatch_error.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
TextureResources candidate;
|
||||
candidate.width = width;
|
||||
candidate.height = height;
|
||||
if (!eglBindAPI(EGL_OPENGL_ES_API) || !eglMakeCurrent(mpv_display, EGL_NO_SURFACE, EGL_NO_SURFACE, mpv_context)) {
|
||||
RestoreOrReleaseContext(flutter_display, flutter_draw, flutter_read, flutter_context, flutter_api, mpv_display);
|
||||
return SetError(error, "Failed to activate video EGL context");
|
||||
}
|
||||
|
||||
ClearGlErrors();
|
||||
glGenTextures(1, &candidate.mpv_texture);
|
||||
glBindTexture(GL_TEXTURE_2D, candidate.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, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
glGenFramebuffers(1, &candidate.mpv_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, candidate.mpv_fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, candidate.mpv_texture, 0);
|
||||
bool framebuffer_complete = candidate.mpv_texture != 0 && candidate.mpv_fbo != 0 &&
|
||||
glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE &&
|
||||
glGetError() == GL_NO_ERROR;
|
||||
if (framebuffer_complete) {
|
||||
candidate.egl_image = self->image_dispatch->Create(
|
||||
mpv_display, mpv_context, reinterpret_cast<EGLClientBuffer>(static_cast<uintptr_t>(candidate.mpv_texture)));
|
||||
}
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glFlush();
|
||||
framebuffer_complete = framebuffer_complete && glGetError() == GL_NO_ERROR;
|
||||
|
||||
if (!RestoreContext(flutter_display, flutter_draw, flutter_read, flutter_context, flutter_api, error)) {
|
||||
CleanupResourceSet(self, &candidate, flutter_display, flutter_draw, flutter_read, flutter_context, flutter_api);
|
||||
RetireIncompleteCandidate(self, candidate);
|
||||
return false;
|
||||
}
|
||||
if (!framebuffer_complete || candidate.egl_image == EGL_NO_IMAGE_KHR) {
|
||||
CleanupResourceSet(self, &candidate, flutter_display, flutter_draw, flutter_read, flutter_context, flutter_api);
|
||||
RetireIncompleteCandidate(self, candidate);
|
||||
return SetError(error, "Failed to create a complete video framebuffer");
|
||||
}
|
||||
|
||||
ClearGlErrors();
|
||||
glGenTextures(1, &candidate.flutter_texture);
|
||||
glBindTexture(GL_TEXTURE_2D, candidate.flutter_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);
|
||||
self->image_dispatch->image_target_texture(GL_TEXTURE_2D, reinterpret_cast<GLeglImageOES>(candidate.egl_image));
|
||||
const bool flutter_texture_complete = candidate.flutter_texture != 0 && glGetError() == GL_NO_ERROR;
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
if (!flutter_texture_complete) {
|
||||
CleanupResourceSet(self, &candidate, flutter_display, flutter_draw, flutter_read, flutter_context, flutter_api);
|
||||
RetireIncompleteCandidate(self, candidate);
|
||||
return SetError(error, "Failed to bind the shared video image");
|
||||
}
|
||||
|
||||
if (self->active->complete()) self->retired->push_back(*self->active);
|
||||
*self->active = candidate;
|
||||
CleanupRetired(self);
|
||||
return true;
|
||||
}
|
||||
|
||||
static gboolean MpvTexturePopulate(
|
||||
FlTextureGL* texture, uint32_t* target, uint32_t* name, uint32_t* width, uint32_t* height, GError** error) {
|
||||
MpvTexture* self = MPV_TEXTURE(texture);
|
||||
g_mutex_lock(&self->mutex);
|
||||
if (self->disposed || !self->player) {
|
||||
g_object_ref(self);
|
||||
g_mutex_unlock(&self->mutex);
|
||||
SignalBootstrap(self, FALSE, "Video texture was disposed");
|
||||
g_object_unref(self);
|
||||
return SetError(error, "Video texture was disposed");
|
||||
}
|
||||
if (!self->player->HasRenderContext() && !self->player->InitRenderContext()) {
|
||||
g_mutex_unlock(&self->mutex);
|
||||
return SetError(error, "Failed to create video render context");
|
||||
}
|
||||
|
||||
GtkAllocation allocation;
|
||||
gtk_widget_get_allocation(GTK_WIDGET(self->view), &allocation);
|
||||
const int scale = gtk_widget_get_scale_factor(GTK_WIDGET(self->view));
|
||||
const int32_t requested_width = allocation.width * scale;
|
||||
const int32_t requested_height = allocation.height * scale;
|
||||
if (requested_width <= 0 || requested_height <= 0) {
|
||||
g_mutex_unlock(&self->mutex);
|
||||
return SetError(error, "Video surface has no drawable size");
|
||||
}
|
||||
// GL/EGL failures during the first populate are not terminal. Flutter may
|
||||
// call populate again while waitForVideoReady owns the bounded deadline.
|
||||
if (!EnsureTextures(self, requested_width, requested_height, error)) {
|
||||
g_mutex_unlock(&self->mutex);
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
const EGLDisplay flutter_display = eglGetCurrentDisplay();
|
||||
const EGLContext flutter_context = eglGetCurrentContext();
|
||||
const EGLSurface flutter_draw = eglGetCurrentSurface(EGL_DRAW);
|
||||
const EGLSurface flutter_read = eglGetCurrentSurface(EGL_READ);
|
||||
const EGLenum flutter_api = eglQueryAPI();
|
||||
const EGLDisplay mpv_display = self->player->GetEglDisplay();
|
||||
const EGLContext mpv_context = self->player->GetEglContext();
|
||||
if (!eglBindAPI(EGL_OPENGL_ES_API) || !eglMakeCurrent(mpv_display, EGL_NO_SURFACE, EGL_NO_SURFACE, mpv_context)) {
|
||||
RestoreOrReleaseContext(flutter_display, flutter_draw, flutter_read, flutter_context, flutter_api, mpv_display);
|
||||
g_mutex_unlock(&self->mutex);
|
||||
return SetError(error, "Failed to activate video EGL context");
|
||||
}
|
||||
|
||||
ClearGlErrors();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, self->active->mpv_fbo);
|
||||
self->player->ClearRedrawFlag();
|
||||
self->player->Render(requested_width, requested_height, static_cast<int>(self->active->mpv_fbo));
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glFlush();
|
||||
const bool render_succeeded = glGetError() == GL_NO_ERROR;
|
||||
if (!RestoreContext(flutter_display, flutter_draw, flutter_read, flutter_context, flutter_api, error)) {
|
||||
RestoreOrReleaseContext(flutter_display, flutter_draw, flutter_read, flutter_context, flutter_api, mpv_display);
|
||||
g_mutex_unlock(&self->mutex);
|
||||
return FALSE;
|
||||
}
|
||||
if (!render_succeeded) {
|
||||
g_mutex_unlock(&self->mutex);
|
||||
return SetError(error, "Video render operation failed");
|
||||
}
|
||||
|
||||
*target = GL_TEXTURE_2D;
|
||||
*name = self->active->flutter_texture;
|
||||
*width = static_cast<uint32_t>(requested_width);
|
||||
*height = static_cast<uint32_t>(requested_height);
|
||||
g_object_ref(self);
|
||||
g_mutex_unlock(&self->mutex);
|
||||
SignalBootstrap(self, TRUE, nullptr);
|
||||
g_object_unref(self);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void MpvTextureFinalize(GObject* object) {
|
||||
MpvTexture* self = MPV_TEXTURE(object);
|
||||
if (self->ready_destroy_notify && self->ready_user_data) {
|
||||
self->ready_destroy_notify(self->ready_user_data);
|
||||
}
|
||||
g_free(self->bootstrap_error);
|
||||
delete self->active;
|
||||
delete self->retired;
|
||||
delete self->image_dispatch;
|
||||
g_mutex_clear(&self->bootstrap_mutex);
|
||||
g_mutex_clear(&self->mutex);
|
||||
G_OBJECT_CLASS(mpv_texture_parent_class)->finalize(object);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static void mpv_texture_class_init(MpvTextureClass* klass) {
|
||||
FL_TEXTURE_GL_CLASS(klass)->populate = MpvTexturePopulate;
|
||||
G_OBJECT_CLASS(klass)->finalize = MpvTextureFinalize;
|
||||
}
|
||||
|
||||
static void mpv_texture_init(MpvTexture* self) {
|
||||
self->player = nullptr;
|
||||
self->registrar = nullptr;
|
||||
self->view = nullptr;
|
||||
g_mutex_init(&self->mutex);
|
||||
self->disposed = false;
|
||||
self->active = new TextureResources();
|
||||
self->retired = new std::vector<TextureResources>();
|
||||
self->image_dispatch = new mpv::GpuImageDispatch();
|
||||
self->flutter_display = EGL_NO_DISPLAY;
|
||||
self->flutter_share_context = EGL_NO_CONTEXT;
|
||||
self->flutter_cleanup_context = EGL_NO_CONTEXT;
|
||||
g_mutex_init(&self->bootstrap_mutex);
|
||||
self->bootstrap_state = 0;
|
||||
self->bootstrap_error = nullptr;
|
||||
self->ready_callback = nullptr;
|
||||
self->ready_user_data = nullptr;
|
||||
self->ready_destroy_notify = nullptr;
|
||||
}
|
||||
|
||||
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_set_ready_callback(
|
||||
MpvTexture* self, MpvTextureReadyCallback callback, gpointer user_data, GDestroyNotify destroy_notify) {
|
||||
gboolean success = FALSE;
|
||||
const gchar* message = nullptr;
|
||||
bool complete = false;
|
||||
g_mutex_lock(&self->bootstrap_mutex);
|
||||
self->ready_callback = callback;
|
||||
self->ready_user_data = user_data;
|
||||
self->ready_destroy_notify = destroy_notify;
|
||||
if (self->bootstrap_state != 0) {
|
||||
complete = true;
|
||||
success = self->bootstrap_state == 1;
|
||||
message = self->bootstrap_error;
|
||||
}
|
||||
g_mutex_unlock(&self->bootstrap_mutex);
|
||||
if (complete && callback) callback(success, message, user_data);
|
||||
}
|
||||
|
||||
void mpv_texture_mark_frame_available(MpvTexture* self) {
|
||||
if (!self) return;
|
||||
g_mutex_lock(&self->mutex);
|
||||
FlTextureRegistrar* registrar = self->disposed ? nullptr : self->registrar;
|
||||
g_mutex_unlock(&self->mutex);
|
||||
if (registrar) {
|
||||
fl_texture_registrar_mark_texture_frame_available(registrar, FL_TEXTURE(self));
|
||||
}
|
||||
}
|
||||
|
||||
void mpv_texture_dispose(MpvTexture* self) {
|
||||
if (!self) return;
|
||||
g_mutex_lock(&self->mutex);
|
||||
if (self->disposed) {
|
||||
g_mutex_unlock(&self->mutex);
|
||||
return;
|
||||
}
|
||||
self->disposed = true;
|
||||
SignalBootstrap(self, FALSE, "Video initialization was cancelled");
|
||||
|
||||
if (self->player) {
|
||||
const EGLDisplay flutter_display = eglGetCurrentDisplay();
|
||||
const EGLContext flutter_context = eglGetCurrentContext();
|
||||
const EGLSurface flutter_draw = eglGetCurrentSurface(EGL_DRAW);
|
||||
const EGLSurface flutter_read = eglGetCurrentSurface(EGL_READ);
|
||||
const EGLenum flutter_api = eglQueryAPI();
|
||||
CleanupResourceSet(self, self->active, flutter_display, flutter_draw, flutter_read, flutter_context, flutter_api);
|
||||
for (auto& resources : *self->retired) {
|
||||
CleanupResourceSet(self, &resources, flutter_display, flutter_draw, flutter_read, flutter_context, flutter_api);
|
||||
}
|
||||
DestroyFlutterCleanupContext(self);
|
||||
const auto leaked_sets =
|
||||
static_cast<size_t>(!ResourceSetEmpty(*self->active)) +
|
||||
static_cast<size_t>(std::count_if(self->retired->begin(), self->retired->end(), [](const auto& resources) {
|
||||
return !ResourceSetEmpty(resources);
|
||||
}));
|
||||
if (leaked_sets != 0) {
|
||||
g_warning("MPV texture: %zu resource set(s) could not be released before disposal", leaked_sets);
|
||||
}
|
||||
}
|
||||
*self->active = TextureResources{};
|
||||
self->retired->clear();
|
||||
self->player = nullptr;
|
||||
self->registrar = nullptr;
|
||||
self->view = nullptr;
|
||||
g_mutex_unlock(&self->mutex);
|
||||
}
|
||||
|
||||
int64_t mpv_texture_get_id(MpvTexture* self) { return fl_texture_get_id(FL_TEXTURE(self)); }
|
||||
@@ -0,0 +1,34 @@
|
||||
#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);
|
||||
|
||||
typedef void (*MpvTextureReadyCallback)(gboolean success, const gchar* error_message, gpointer user_data);
|
||||
|
||||
/// Installs the one-shot video bootstrap result callback.
|
||||
void mpv_texture_set_ready_callback(
|
||||
MpvTexture* self, MpvTextureReadyCallback callback, gpointer user_data, GDestroyNotify destroy_notify);
|
||||
|
||||
/// 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_
|
||||
@@ -12,7 +12,9 @@
|
||||
// On Wayland the mpv video plane is a wl_subsurface stacked *below* this
|
||||
// window's surface, so the window needs an alpha channel for it to show
|
||||
// through. Harmless when the plane is unavailable: the Flutter UI paints
|
||||
// opaque anyway, and X11 sessions refuse video at initialize by design.
|
||||
// opaque anyway, and sessions that cannot host the plane (X11/XWayland)
|
||||
// fall back to the Flutter-texture path, which composites through Flutter
|
||||
// and needs no alpha channel.
|
||||
static void enable_video_plane_transparency(GtkWindow* window, FlView* view) {
|
||||
#ifdef GDK_WINDOWING_WAYLAND
|
||||
GdkDisplay* display = gtk_widget_get_display(GTK_WIDGET(window));
|
||||
@@ -85,8 +87,9 @@ static void my_application_activate(GApplication* application) {
|
||||
// Register Flutter plugins.
|
||||
fl_register_plugins(FL_PLUGIN_REGISTRY(self->flutter_view));
|
||||
|
||||
// Register the MPV plugin. Video goes to the native Wayland plane; there is no other path, so a session that
|
||||
// cannot host one is refused by name rather than played into nothing.
|
||||
// Register the MPV plugin. Video prefers the native Wayland plane; sessions
|
||||
// that cannot host one (X11/XWayland, or a failed plane bootstrap) fall back
|
||||
// to the Flutter-texture path in SDR.
|
||||
FlPluginRegistrar* registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(FL_PLUGIN_REGISTRY(self->flutter_view), "MpvPlugin");
|
||||
mpv_plugin_register_with_registrar(registrar);
|
||||
|
||||
Reference in New Issue
Block a user