fix(native): bound cross-platform lifecycle ownership
This commit is contained in:
@@ -10,6 +10,7 @@ add_executable(${BINARY_NAME}
|
||||
"main.cc"
|
||||
"my_application.cc"
|
||||
"mpv/mpv_player.cc"
|
||||
"mpv/mpv_gpu_bootstrap.cc"
|
||||
"mpv/mpv_plugin.cc"
|
||||
"mpv/mpv_texture.cc"
|
||||
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||
@@ -31,7 +32,7 @@ pkg_check_modules(EPOXY REQUIRED IMPORTED_TARGET epoxy)
|
||||
# Build simdutf as a static library from the single-header amalgamation.
|
||||
add_library(simdutf STATIC "${simdutf_SOURCE_DIR}/simdutf.cpp")
|
||||
target_include_directories(simdutf PUBLIC "${simdutf_SOURCE_DIR}")
|
||||
target_compile_features(simdutf PUBLIC cxx_std_17)
|
||||
target_compile_features(simdutf PUBLIC cxx_std_14)
|
||||
# Suppress warnings in third-party code
|
||||
target_compile_options(simdutf PRIVATE -w)
|
||||
|
||||
@@ -45,17 +46,30 @@ target_link_libraries(${BINARY_NAME} PRIVATE simdutf)
|
||||
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}/../shared/cpp")
|
||||
|
||||
function(check_mpv_sanitizer_support SANITIZER_FLAG RESULT_VARIABLE)
|
||||
include(CheckCXXSourceCompiles)
|
||||
# Force an executable try-compile: sanitizer availability depends on the
|
||||
# runtime being linkable, not just on the compiler accepting the flag.
|
||||
set(CMAKE_TRY_COMPILE_TARGET_TYPE EXECUTABLE)
|
||||
set(CMAKE_REQUIRED_FLAGS "${SANITIZER_FLAG}")
|
||||
set(CMAKE_REQUIRED_LIBRARIES "${SANITIZER_FLAG}")
|
||||
check_cxx_source_compiles("int main() { return 0; }" ${RESULT_VARIABLE})
|
||||
set(${RESULT_VARIABLE} "${${RESULT_VARIABLE}}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
option(PLEZY_BUILD_MPV_PLAYER_LIFECYCLE_TESTS
|
||||
"Build the focused Linux mpv callback lifecycle test" OFF)
|
||||
if(PLEZY_BUILD_MPV_PLAYER_LIFECYCLE_TESTS)
|
||||
enable_testing()
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
add_executable(mpv_player_lifecycle_test
|
||||
"mpv/mpv_player.cc"
|
||||
"mpv/mpv_gpu_bootstrap.cc"
|
||||
"mpv/mpv_texture.cc"
|
||||
"mpv/mpv_player_lifecycle_test.cc"
|
||||
)
|
||||
apply_standard_settings(mpv_player_lifecycle_test)
|
||||
target_compile_definitions(mpv_player_lifecycle_test PRIVATE PLEZY_MPV_PLAYER_LIFECYCLE_TEST=1)
|
||||
target_link_libraries(mpv_player_lifecycle_test PRIVATE flutter)
|
||||
target_link_libraries(mpv_player_lifecycle_test PRIVATE PkgConfig::GTK)
|
||||
target_link_libraries(mpv_player_lifecycle_test PRIVATE PkgConfig::MPV)
|
||||
@@ -68,8 +82,8 @@ if(PLEZY_BUILD_MPV_PLAYER_LIFECYCLE_TESTS)
|
||||
option(PLEZY_MPV_LIFECYCLE_SANITIZERS
|
||||
"Enable ASan and UBSan for the focused mpv lifecycle test" ON)
|
||||
if(PLEZY_MPV_LIFECYCLE_SANITIZERS AND CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU")
|
||||
include(CheckCXXCompilerFlag)
|
||||
check_cxx_compiler_flag("-fsanitize=address,undefined" MPV_LIFECYCLE_SANITIZERS_SUPPORTED)
|
||||
check_mpv_sanitizer_support(
|
||||
"-fsanitize=address,undefined" MPV_LIFECYCLE_SANITIZERS_SUPPORTED)
|
||||
if(MPV_LIFECYCLE_SANITIZERS_SUPPORTED)
|
||||
target_compile_options(mpv_player_lifecycle_test PRIVATE -fno-omit-frame-pointer -fsanitize=address,undefined)
|
||||
target_link_options(mpv_player_lifecycle_test PRIVATE -fsanitize=address,undefined)
|
||||
@@ -77,20 +91,63 @@ if(PLEZY_BUILD_MPV_PLAYER_LIFECYCLE_TESTS)
|
||||
endif()
|
||||
|
||||
add_test(NAME mpv_player_lifecycle_test COMMAND mpv_player_lifecycle_test)
|
||||
set_tests_properties(mpv_player_lifecycle_test PROPERTIES TIMEOUT 30)
|
||||
endif()
|
||||
|
||||
option(PLEZY_BUILD_MPV_PROPERTY_CONTRACT_TESTS
|
||||
"Build the focused desktop mpv property-result contract test" OFF)
|
||||
if(PLEZY_BUILD_MPV_PROPERTY_CONTRACT_TESTS)
|
||||
enable_testing()
|
||||
find_package(Threads REQUIRED)
|
||||
option(PLEZY_BUILD_MPV_RELIABILITY_TESTS
|
||||
"Build focused Linux mpv registry and GPU bootstrap tests" OFF)
|
||||
set(PLEZY_MPV_RELIABILITY_SANITIZER "none" CACHE STRING
|
||||
"Sanitizer for focused mpv reliability tests: none, address, or thread")
|
||||
set_property(CACHE PLEZY_MPV_RELIABILITY_SANITIZER PROPERTY STRINGS none address thread)
|
||||
|
||||
function(apply_mpv_reliability_sanitizer TARGET)
|
||||
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU" OR
|
||||
PLEZY_MPV_RELIABILITY_SANITIZER STREQUAL "none")
|
||||
return()
|
||||
endif()
|
||||
if(PLEZY_MPV_RELIABILITY_SANITIZER STREQUAL "address")
|
||||
set(SANITIZER_FLAG "-fsanitize=address,undefined")
|
||||
set(SANITIZER_SUPPORT_VARIABLE MPV_RELIABILITY_ADDRESS_SANITIZER_SUPPORTED)
|
||||
elseif(PLEZY_MPV_RELIABILITY_SANITIZER STREQUAL "thread")
|
||||
set(SANITIZER_FLAG "-fsanitize=thread")
|
||||
set(SANITIZER_SUPPORT_VARIABLE MPV_RELIABILITY_THREAD_SANITIZER_SUPPORTED)
|
||||
else()
|
||||
message(FATAL_ERROR "Unknown PLEZY_MPV_RELIABILITY_SANITIZER value")
|
||||
endif()
|
||||
check_mpv_sanitizer_support("${SANITIZER_FLAG}" ${SANITIZER_SUPPORT_VARIABLE})
|
||||
if(NOT ${SANITIZER_SUPPORT_VARIABLE})
|
||||
message(WARNING
|
||||
"${PLEZY_MPV_RELIABILITY_SANITIZER} sanitizer is unavailable; focused tests will be unsanitized")
|
||||
return()
|
||||
endif()
|
||||
target_compile_options(${TARGET} PRIVATE -fno-omit-frame-pointer ${SANITIZER_FLAG})
|
||||
target_link_options(${TARGET} PRIVATE ${SANITIZER_FLAG})
|
||||
endfunction()
|
||||
|
||||
if(PLEZY_BUILD_MPV_PROPERTY_CONTRACT_TESTS OR PLEZY_BUILD_MPV_RELIABILITY_TESTS)
|
||||
find_package(Threads REQUIRED)
|
||||
add_executable(mpv_property_result_contract_test
|
||||
"../../shared/mpv/mpv_player_common_test.cpp"
|
||||
)
|
||||
apply_standard_settings(mpv_property_result_contract_test)
|
||||
target_compile_features(mpv_property_result_contract_test PRIVATE cxx_std_14)
|
||||
target_link_libraries(mpv_property_result_contract_test PRIVATE PkgConfig::MPV Threads::Threads)
|
||||
target_include_directories(mpv_property_result_contract_test PRIVATE "../../shared/mpv")
|
||||
|
||||
apply_mpv_reliability_sanitizer(mpv_property_result_contract_test)
|
||||
add_test(NAME mpv_property_result_contract_test COMMAND mpv_property_result_contract_test)
|
||||
endif()
|
||||
|
||||
if(PLEZY_BUILD_MPV_RELIABILITY_TESTS)
|
||||
add_executable(mpv_gpu_bootstrap_test
|
||||
"mpv/mpv_gpu_bootstrap.cc"
|
||||
"mpv/mpv_gpu_bootstrap_test.cc"
|
||||
)
|
||||
apply_standard_settings(mpv_gpu_bootstrap_test)
|
||||
target_compile_features(mpv_gpu_bootstrap_test PRIVATE cxx_std_14)
|
||||
target_link_libraries(mpv_gpu_bootstrap_test PRIVATE PkgConfig::EPOXY)
|
||||
target_include_directories(mpv_gpu_bootstrap_test PRIVATE "mpv")
|
||||
apply_mpv_reliability_sanitizer(mpv_gpu_bootstrap_test)
|
||||
add_test(NAME mpv_gpu_bootstrap_test COMMAND mpv_gpu_bootstrap_test)
|
||||
endif()
|
||||
|
||||
@@ -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_
|
||||
@@ -0,0 +1,129 @@
|
||||
#include "mpv_gpu_bootstrap.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
int create_calls = 0;
|
||||
int destroy_calls = 0;
|
||||
int failures = 0;
|
||||
|
||||
void Expect(bool condition, const char* expression, int line) {
|
||||
if (condition) return;
|
||||
std::cerr << "line " << line << ": check failed: " << expression << '\n';
|
||||
++failures;
|
||||
}
|
||||
|
||||
#define EXPECT(condition) Expect(static_cast<bool>(condition), #condition, __LINE__)
|
||||
|
||||
EGLImageKHR CreateImageKhr(EGLDisplay, EGLContext, EGLenum, EGLClientBuffer, const EGLint*) {
|
||||
++create_calls;
|
||||
return reinterpret_cast<EGLImageKHR>(0x1234);
|
||||
}
|
||||
|
||||
EGLBoolean DestroyImageKhr(EGLDisplay, EGLImageKHR image) {
|
||||
EXPECT(image == reinterpret_cast<EGLImageKHR>(0x1234));
|
||||
++destroy_calls;
|
||||
return EGL_TRUE;
|
||||
}
|
||||
|
||||
void BindImage(GLenum, GLeglImageOES) {}
|
||||
|
||||
template <typename Function>
|
||||
void* Address(Function function) {
|
||||
return reinterpret_cast<void*>(function);
|
||||
}
|
||||
|
||||
mpv::GpuBootstrapProbe SupportedKhrProbe() {
|
||||
mpv::GpuBootstrapProbe probe;
|
||||
probe.egl_major = 1;
|
||||
probe.egl_minor = 4;
|
||||
probe.egl_extensions = "EGL_KHR_surfaceless_context EGL_KHR_image_base";
|
||||
probe.gl_extensions = "GL_EXT_texture GL_OES_EGL_image";
|
||||
probe.create_image_khr = Address(CreateImageKhr);
|
||||
probe.destroy_image_khr = Address(DestroyImageKhr);
|
||||
probe.image_target_texture = Address(BindImage);
|
||||
return probe;
|
||||
}
|
||||
|
||||
void TestKhrCapabilitiesFailClosed() {
|
||||
std::string error;
|
||||
auto probe = SupportedKhrProbe();
|
||||
EXPECT(mpv::ValidateGpuBootstrapProbe(probe, &error));
|
||||
EXPECT(error.empty());
|
||||
|
||||
probe.egl_extensions = "EGL_KHR_image_base";
|
||||
EXPECT(!mpv::ValidateGpuBootstrapProbe(probe, &error));
|
||||
probe = SupportedKhrProbe();
|
||||
probe.egl_extensions = "EGL_KHR_surfaceless_context EGL_KHR_image";
|
||||
EXPECT(mpv::ValidateGpuBootstrapProbe(probe, &error));
|
||||
probe = SupportedKhrProbe();
|
||||
probe.egl_extensions = "EGL_KHR_surfaceless_context EGL_KHR_image_suffix";
|
||||
EXPECT(!mpv::ValidateGpuBootstrapProbe(probe, &error));
|
||||
probe = SupportedKhrProbe();
|
||||
probe.egl_extensions = "EGL_KHR_surfaceless_context";
|
||||
EXPECT(!mpv::ValidateGpuBootstrapProbe(probe, &error));
|
||||
probe = SupportedKhrProbe();
|
||||
probe.gl_extensions = "GL_OES_EGL_image_external";
|
||||
EXPECT(!mpv::ValidateGpuBootstrapProbe(probe, &error));
|
||||
|
||||
probe = SupportedKhrProbe();
|
||||
probe.create_image_khr = nullptr;
|
||||
EXPECT(!mpv::ValidateGpuBootstrapProbe(probe, &error));
|
||||
probe = SupportedKhrProbe();
|
||||
probe.destroy_image_khr = nullptr;
|
||||
EXPECT(!mpv::ValidateGpuBootstrapProbe(probe, &error));
|
||||
probe = SupportedKhrProbe();
|
||||
probe.image_target_texture = nullptr;
|
||||
EXPECT(!mpv::ValidateGpuBootstrapProbe(probe, &error));
|
||||
}
|
||||
|
||||
void TestCoreCapabilities() {
|
||||
std::string error;
|
||||
auto probe = SupportedKhrProbe();
|
||||
probe.egl_major = 1;
|
||||
probe.egl_minor = 5;
|
||||
probe.egl_extensions = "";
|
||||
probe.create_image_khr = nullptr;
|
||||
probe.destroy_image_khr = nullptr;
|
||||
probe.create_image_core = Address(CreateImageKhr);
|
||||
probe.destroy_image_core = Address(DestroyImageKhr);
|
||||
EXPECT(mpv::ValidateGpuBootstrapProbe(probe, &error));
|
||||
|
||||
probe.create_image_core = nullptr;
|
||||
EXPECT(!mpv::ValidateGpuBootstrapProbe(probe, &error));
|
||||
probe = SupportedKhrProbe();
|
||||
probe.egl_major = 0;
|
||||
probe.egl_minor = 0;
|
||||
probe.egl_extensions = "";
|
||||
EXPECT(!mpv::ValidateGpuBootstrapProbe(probe, &error));
|
||||
}
|
||||
|
||||
void TestDispatchChecksBeforeCalls() {
|
||||
mpv::GpuImageDispatch dispatch;
|
||||
EXPECT(!dispatch);
|
||||
EXPECT(dispatch.Create(EGL_NO_DISPLAY, EGL_NO_CONTEXT, nullptr) == EGL_NO_IMAGE_KHR);
|
||||
EXPECT(!dispatch.Destroy(EGL_NO_DISPLAY, reinterpret_cast<EGLImageKHR>(0x1234)));
|
||||
EXPECT(create_calls == 0);
|
||||
EXPECT(destroy_calls == 0);
|
||||
|
||||
dispatch.create_image_khr = CreateImageKhr;
|
||||
dispatch.destroy_image_khr = DestroyImageKhr;
|
||||
dispatch.image_target_texture = BindImage;
|
||||
EXPECT(dispatch);
|
||||
const auto image = dispatch.Create(EGL_NO_DISPLAY, EGL_NO_CONTEXT, nullptr);
|
||||
EXPECT(image == reinterpret_cast<EGLImageKHR>(0x1234));
|
||||
EXPECT(dispatch.Destroy(EGL_NO_DISPLAY, image));
|
||||
EXPECT(create_calls == 1);
|
||||
EXPECT(destroy_calls == 1);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
TestKhrCapabilitiesFailClosed();
|
||||
TestCoreCapabilities();
|
||||
TestDispatchChecksBeforeCalls();
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
+463
-152
@@ -10,10 +10,25 @@
|
||||
#ifdef GDK_WINDOWING_WAYLAND
|
||||
#include <gdk/gdkwayland.h>
|
||||
#endif
|
||||
#include <clocale>
|
||||
#include <locale.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
|
||||
#include "sanitize_utf8.h"
|
||||
|
||||
namespace {
|
||||
|
||||
bool EnsureProcessNumericLocale() {
|
||||
// libmpv parses numeric options on worker threads, so a thread-local locale
|
||||
// is insufficient. This process-wide setting intentionally remains in force
|
||||
// for the rest of the process after the first player starts.
|
||||
static const bool configured = setlocale(LC_NUMERIC, "C") != nullptr;
|
||||
return configured;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Flutter on Linux uses EGL (OpenGL ES) for both X11 and Wayland.
|
||||
static void* get_opengl_proc_address(void* ctx, const char* name) {
|
||||
(void)ctx;
|
||||
@@ -21,6 +36,171 @@ static void* get_opengl_proc_address(void* ctx, const char* name) {
|
||||
}
|
||||
|
||||
namespace mpv {
|
||||
namespace {
|
||||
|
||||
NativeRenderTeardownOperations ProductionTeardownOperations() {
|
||||
return {
|
||||
[](EGLDisplay display, EGLContext context) {
|
||||
if (!eglBindAPI(EGL_OPENGL_ES_API) || !eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, context)) {
|
||||
g_warning("MPV: Failed to activate EGL context for teardown: 0x%x", eglGetError());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[](EGLDisplay display) {
|
||||
if (!eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)) {
|
||||
g_warning("MPV: Failed to release EGL context during teardown: 0x%x", eglGetError());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[](EGLDisplay display, EGLContext context) {
|
||||
if (!eglDestroyContext(display, context)) {
|
||||
g_warning("MPV: Failed to destroy EGL context during teardown: 0x%x", eglGetError());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[](mpv_render_context* render) { mpv_render_context_free(render); },
|
||||
[](mpv_handle* handle) { mpv_terminate_destroy(handle); },
|
||||
};
|
||||
}
|
||||
|
||||
#ifdef PLEZY_MPV_PLAYER_LIFECYCLE_TEST
|
||||
NativeRenderTeardownOperations*& TestTeardownOperationsOverride() {
|
||||
static NativeRenderTeardownOperations* operations = nullptr;
|
||||
return operations;
|
||||
}
|
||||
#endif
|
||||
|
||||
class NativeRenderTeardownQueue {
|
||||
public:
|
||||
static NativeRenderTeardownQueue& Instance() {
|
||||
// Native driver/libmpv teardown can block indefinitely. Keep both the
|
||||
// queue and its worker state alive until the OS ends the process so static
|
||||
// destruction never joins the worker or invalidates state it may access.
|
||||
static NativeRenderTeardownQueue* const queue = new NativeRenderTeardownQueue();
|
||||
return *queue;
|
||||
}
|
||||
|
||||
void Enqueue(NativeRenderTeardownBatch batch) {
|
||||
if (batch.resources.empty() && !batch.handle) return;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
batches_.push_back(std::move(batch));
|
||||
++generation_;
|
||||
}
|
||||
condition_.notify_one();
|
||||
}
|
||||
|
||||
void Retry() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
++generation_;
|
||||
}
|
||||
condition_.notify_one();
|
||||
}
|
||||
|
||||
private:
|
||||
NativeRenderTeardownQueue() : worker_([this]() { Run(); }) {}
|
||||
|
||||
void Run() {
|
||||
#ifdef PLEZY_MPV_PLAYER_LIFECYCLE_TEST
|
||||
const NativeRenderTeardownOperations operations =
|
||||
TestTeardownOperationsOverride() ? *TestTeardownOperationsOverride() : ProductionTeardownOperations();
|
||||
#else
|
||||
const NativeRenderTeardownOperations operations = ProductionTeardownOperations();
|
||||
#endif
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
for (;;) {
|
||||
condition_.wait(lock, [this]() { return !batches_.empty(); });
|
||||
const uint64_t observed_generation = generation_;
|
||||
std::vector<NativeRenderTeardownBatch> work = std::move(batches_);
|
||||
batches_.clear();
|
||||
|
||||
// EGL activation and mpv shutdown can block in a driver. Keep queue
|
||||
// admission independent so replacement initialization and disposal only
|
||||
// pay the short ownership-transfer critical section.
|
||||
lock.unlock();
|
||||
std::vector<NativeRenderTeardownBatch> retry;
|
||||
for (auto& batch : work) {
|
||||
if (!TryReleaseNativeRenderTeardown(batch, operations)) retry.push_back(std::move(batch));
|
||||
}
|
||||
lock.lock();
|
||||
for (auto& batch : retry) batches_.push_back(std::move(batch));
|
||||
|
||||
if (batches_.empty()) continue;
|
||||
|
||||
condition_.wait_for(lock, std::chrono::milliseconds(100), [this, observed_generation]() {
|
||||
return generation_ != observed_generation;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
std::mutex mutex_;
|
||||
std::condition_variable condition_;
|
||||
std::vector<NativeRenderTeardownBatch> batches_;
|
||||
uint64_t generation_ = 0;
|
||||
std::thread worker_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
#ifdef PLEZY_MPV_PLAYER_LIFECYCLE_TEST
|
||||
void ConfigureNativeRenderTeardownQueueForTesting(NativeRenderTeardownOperations operations) {
|
||||
auto*& configured = TestTeardownOperationsOverride();
|
||||
if (configured) {
|
||||
*configured = std::move(operations);
|
||||
} else {
|
||||
configured = new NativeRenderTeardownOperations(std::move(operations));
|
||||
}
|
||||
}
|
||||
|
||||
void EnqueueNativeRenderTeardownForTesting(NativeRenderTeardownBatch batch) {
|
||||
NativeRenderTeardownQueue::Instance().Enqueue(std::move(batch));
|
||||
}
|
||||
#endif
|
||||
|
||||
bool TryReleaseNativeRenderTeardown(
|
||||
NativeRenderTeardownBatch& batch, const NativeRenderTeardownOperations& operations) {
|
||||
for (auto it = batch.resources.begin(); it != batch.resources.end();) {
|
||||
if (it->context == EGL_NO_CONTEXT || !operations.make_current(it->display, it->context)) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (it->render) {
|
||||
operations.free_render(it->render);
|
||||
it->render = nullptr;
|
||||
}
|
||||
if (!operations.release_current(it->display)) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
if (!operations.destroy_context(it->display, it->context)) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
it = batch.resources.erase(it);
|
||||
}
|
||||
|
||||
if (!batch.resources.empty()) return false;
|
||||
if (batch.handle) {
|
||||
operations.terminate_handle(batch.handle);
|
||||
batch.handle = nullptr;
|
||||
}
|
||||
batch.callback_keep_alive.reset();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryReleaseRetainedNativeRenderContexts(
|
||||
std::vector<NativeRenderTeardownResource>& resources, const NativeRenderTeardownOperations& operations) {
|
||||
NativeRenderTeardownBatch batch;
|
||||
batch.resources = std::move(resources);
|
||||
const bool complete = TryReleaseNativeRenderTeardown(batch, operations);
|
||||
resources = std::move(batch.resources);
|
||||
return complete;
|
||||
}
|
||||
|
||||
MpvPlayer::CallbackContext::Lease::Lease(CallbackContext* context, MpvPlayer* player)
|
||||
: context_(context), player_(player) {}
|
||||
@@ -62,9 +242,14 @@ MpvPlayer::CallbackContext::Lease MpvPlayer::CallbackContext::Acquire() {
|
||||
return Lease(this, player_);
|
||||
}
|
||||
|
||||
void MpvPlayer::CallbackContext::WaitUntilDetached() {
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
quiescent_.wait(lock, [this]() { return player_ == nullptr; });
|
||||
}
|
||||
void MpvPlayer::CallbackContext::DetachAndWait() {
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
player_ = nullptr;
|
||||
quiescent_.notify_all();
|
||||
quiescent_.wait(lock, [this]() { return in_flight_ == 0; });
|
||||
}
|
||||
|
||||
@@ -87,13 +272,45 @@ MpvPlayer::MpvPlayer(bool audio_only)
|
||||
|
||||
MpvPlayer::~MpvPlayer() { Dispose(); }
|
||||
|
||||
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_;
|
||||
}
|
||||
|
||||
bool MpvPlayer::IsInitialized() const {
|
||||
std::lock_guard<std::mutex> lock(native_mutex_);
|
||||
return mpv_ != nullptr && (audio_only_ || mpv_gl_ != nullptr);
|
||||
}
|
||||
|
||||
bool MpvPlayer::HasMpvHandle() const {
|
||||
std::lock_guard<std::mutex> lock(native_mutex_);
|
||||
return mpv_ != nullptr;
|
||||
}
|
||||
|
||||
bool MpvPlayer::Initialize() {
|
||||
std::lock_guard<std::mutex> lock(native_mutex_);
|
||||
if (disposed_) {
|
||||
g_warning("MPV: initialization requested after disposal");
|
||||
return false;
|
||||
}
|
||||
if (mpv_) {
|
||||
return true; // Already initialized.
|
||||
}
|
||||
|
||||
// MPV requires C locale for numeric formatting
|
||||
std::setlocale(LC_NUMERIC, "C");
|
||||
if (!EnsureProcessNumericLocale()) {
|
||||
g_warning("MPV: Failed to establish the process-wide C numeric locale");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create mpv instance.
|
||||
mpv_ = mpv_create();
|
||||
@@ -151,84 +368,115 @@ bool MpvPlayer::Initialize() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void MpvPlayer::RetryPendingNativeTeardown() { NativeRenderTeardownQueue::Instance().Retry(); }
|
||||
|
||||
bool MpvPlayer::InitRenderContext() {
|
||||
if (audio_only_) {
|
||||
g_warning("MPV: InitRenderContext called on an audio-only player");
|
||||
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; // Already created.
|
||||
}
|
||||
|
||||
if (mpv_gl_) return true;
|
||||
if (!mpv_) {
|
||||
g_warning("MPV: Cannot create render context - mpv not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Capture Flutter's EGL display and create an isolated EGL context.
|
||||
// Flutter on Linux uses EGL for both X11 and Wayland. Running mpv in
|
||||
// an isolated context prevents OpenGL state pollution between mpv and
|
||||
// Flutter, which caused corrupted/blank video on some drivers.
|
||||
EGLDisplay flutter_display = eglGetCurrentDisplay();
|
||||
EGLContext flutter_context = eglGetCurrentContext();
|
||||
|
||||
if (flutter_display == EGL_NO_DISPLAY || flutter_context == EGL_NO_CONTEXT) {
|
||||
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 EGL context available");
|
||||
return false;
|
||||
}
|
||||
|
||||
egl_display_ = flutter_display;
|
||||
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;
|
||||
};
|
||||
if (!retained_render_contexts_.empty()) {
|
||||
const bool released =
|
||||
TryReleaseRetainedNativeRenderContexts(retained_render_contexts_, ProductionTeardownOperations());
|
||||
const bool flutter_restored = restore_flutter();
|
||||
if (!released) {
|
||||
g_warning("MPV: Retained render context still requires a later EGL teardown retry");
|
||||
}
|
||||
if (!flutter_restored) {
|
||||
g_warning("MPV: Failed to restore Flutter EGL state after retained teardown: 0x%x", eglGetError());
|
||||
}
|
||||
if (!released || !flutter_restored) return false;
|
||||
}
|
||||
|
||||
// Query Flutter's EGL config and reuse it for compatibility
|
||||
EGLConfig config = nullptr;
|
||||
EGLint config_id = 0;
|
||||
|
||||
if (!eglQueryContext(egl_display_, flutter_context, EGL_CONFIG_ID, &config_id)) {
|
||||
g_warning("MPV: Failed to query Flutter's EGL config ID");
|
||||
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;
|
||||
EGLint config_attribs[] = {EGL_CONFIG_ID, config_id, EGL_NONE};
|
||||
if (!eglChooseConfig(egl_display_, config_attribs, &config, 1, &num_configs) || num_configs == 0) {
|
||||
g_warning("MPV: Failed to get Flutter's EGL config");
|
||||
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;
|
||||
}
|
||||
|
||||
// Create isolated EGL context (NOT shared with Flutter) to prevent
|
||||
// GL state pollution
|
||||
eglBindAPI(EGL_OPENGL_ES_API);
|
||||
EGLint context_attribs[] = {
|
||||
EGL_CONTEXT_CLIENT_VERSION,
|
||||
2,
|
||||
EGL_NONE,
|
||||
};
|
||||
egl_context_ = eglCreateContext(egl_display_, config, EGL_NO_CONTEXT, context_attribs);
|
||||
if (egl_context_ == EGL_NO_CONTEXT) {
|
||||
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;
|
||||
}
|
||||
|
||||
// Make the isolated context current for mpv render context creation
|
||||
EGLSurface flutter_draw = eglGetCurrentSurface(EGL_DRAW);
|
||||
EGLSurface flutter_read = eglGetCurrentSurface(EGL_READ);
|
||||
eglMakeCurrent(egl_display_, EGL_NO_SURFACE, EGL_NO_SURFACE, egl_context_);
|
||||
|
||||
// Set up OpenGL parameters for mpv.
|
||||
mpv_opengl_init_params gl_init_params{
|
||||
.get_proc_address = get_opengl_proc_address,
|
||||
.get_proc_address_ctx = nullptr,
|
||||
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}, // slot for X11/Wayland display
|
||||
{MPV_RENDER_PARAM_INVALID, nullptr},
|
||||
{MPV_RENDER_PARAM_INVALID, nullptr},
|
||||
};
|
||||
|
||||
// Pass X11/Wayland display for VAAPI hardware acceleration
|
||||
GdkDisplay* gdk_display = gdk_display_get_default();
|
||||
#ifdef GDK_WINDOWING_WAYLAND
|
||||
if (GDK_IS_WAYLAND_DISPLAY(gdk_display)) {
|
||||
@@ -243,21 +491,38 @@ bool MpvPlayer::InitRenderContext() {
|
||||
}
|
||||
#endif
|
||||
|
||||
int err = mpv_render_context_create(&mpv_gl_, mpv_, params);
|
||||
|
||||
// Restore Flutter's context
|
||||
eglMakeCurrent(egl_display_, flutter_draw, flutter_read, flutter_context);
|
||||
|
||||
if (err < 0) {
|
||||
g_warning("MPV: mpv_render_context_create() failed: %s", mpv_error_string(err));
|
||||
eglDestroyContext(egl_display_, egl_context_);
|
||||
egl_context_ = EGL_NO_CONTEXT;
|
||||
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: %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");
|
||||
}
|
||||
bool retained_candidate = false;
|
||||
if (candidate_gl) {
|
||||
if (eglMakeCurrent(flutter_display, EGL_NO_SURFACE, EGL_NO_SURFACE, candidate_context)) {
|
||||
mpv_render_context_free(candidate_gl);
|
||||
} else {
|
||||
g_warning("MPV: Failed to reactivate rejected EGL context: 0x%x; retaining it for teardown", eglGetError());
|
||||
retained_render_contexts_.push_back({candidate_gl, flutter_display, candidate_context});
|
||||
retained_candidate = true;
|
||||
}
|
||||
if (!restore_flutter()) {
|
||||
g_warning("MPV: Failed final Flutter EGL restoration: 0x%x", eglGetError());
|
||||
}
|
||||
}
|
||||
if (!retained_candidate) destroy_candidate_context();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set up render update callback.
|
||||
egl_display_ = flutter_display;
|
||||
egl_context_ = candidate_context;
|
||||
mpv_gl_ = candidate_gl;
|
||||
mpv_render_context_set_update_callback(mpv_gl_, OnMpvRenderUpdate, callback_context_.get());
|
||||
|
||||
g_message("MPV: Render context created with isolated EGL context");
|
||||
return true;
|
||||
}
|
||||
@@ -269,11 +534,21 @@ void MpvPlayer::Dispose() {
|
||||
|
||||
// Stop native producers before revoking access to the player. A callback
|
||||
// already entered on an mpv thread owns a lease and is allowed to finish.
|
||||
if (mpv_gl_) {
|
||||
mpv_render_context_set_update_callback(mpv_gl_, nullptr, nullptr);
|
||||
}
|
||||
if (mpv_) {
|
||||
mpv_set_wakeup_callback(mpv_, nullptr, nullptr);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(native_mutex_);
|
||||
if (mpv_) {
|
||||
const char* stop_command[] = {"stop", nullptr};
|
||||
const int stop_result = mpv_command_async(mpv_, 0, stop_command);
|
||||
if (stop_result < 0) {
|
||||
g_warning("MPV: Failed to enqueue stop during disposal: %s", mpv_error_string(stop_result));
|
||||
}
|
||||
}
|
||||
if (mpv_gl_) {
|
||||
mpv_render_context_set_update_callback(mpv_gl_, nullptr, nullptr);
|
||||
}
|
||||
if (mpv_) {
|
||||
mpv_set_wakeup_callback(mpv_, nullptr, nullptr);
|
||||
}
|
||||
}
|
||||
callback_context_->DetachAndWait();
|
||||
|
||||
@@ -293,59 +568,44 @@ void MpvPlayer::Dispose() {
|
||||
|
||||
RemoveTrackedSources();
|
||||
|
||||
// Native destruction remains off the main thread. Keeping the detached
|
||||
// callback context alive until both mpv objects are gone makes even a late
|
||||
// invocation through mpv's old context pointer harmless.
|
||||
auto* gl = mpv_gl_;
|
||||
auto* handle = mpv_;
|
||||
auto egl_display = egl_display_;
|
||||
auto egl_context = egl_context_;
|
||||
auto callback_context = callback_context_;
|
||||
mpv_gl_ = nullptr;
|
||||
mpv_ = nullptr;
|
||||
egl_display_ = EGL_NO_DISPLAY;
|
||||
egl_context_ = EGL_NO_CONTEXT;
|
||||
|
||||
if (gl || handle || egl_context != EGL_NO_CONTEXT) {
|
||||
std::thread([gl, handle, egl_display, egl_context, callback_context]() {
|
||||
(void)callback_context;
|
||||
if (gl) {
|
||||
if (egl_context != EGL_NO_CONTEXT) {
|
||||
eglMakeCurrent(egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, egl_context);
|
||||
}
|
||||
mpv_render_context_free(gl);
|
||||
}
|
||||
if (handle) {
|
||||
mpv_terminate_destroy(handle);
|
||||
}
|
||||
if (egl_context != EGL_NO_CONTEXT) {
|
||||
eglMakeCurrent(egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
|
||||
eglDestroyContext(egl_display, egl_context);
|
||||
}
|
||||
}).detach();
|
||||
// Transfer every render/context pair and the shared mpv handle to the
|
||||
// managed teardown thread. A failed EGL bind leaves the complete pair in
|
||||
// the queue; the handle cannot be terminated until every pair is gone.
|
||||
NativeRenderTeardownBatch teardown;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(native_mutex_);
|
||||
teardown.resources = std::move(retained_render_contexts_);
|
||||
if (mpv_gl_ || egl_context_ != EGL_NO_CONTEXT) {
|
||||
teardown.resources.push_back({mpv_gl_, egl_display_, egl_context_});
|
||||
}
|
||||
teardown.handle = mpv_;
|
||||
teardown.callback_keep_alive = callback_context_;
|
||||
mpv_gl_ = nullptr;
|
||||
mpv_ = nullptr;
|
||||
egl_display_ = EGL_NO_DISPLAY;
|
||||
egl_context_ = EGL_NO_CONTEXT;
|
||||
}
|
||||
NativeRenderTeardownQueue::Instance().Enqueue(std::move(teardown));
|
||||
|
||||
observed_properties_.Clear();
|
||||
}
|
||||
|
||||
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{
|
||||
.fbo = fbo,
|
||||
.w = width,
|
||||
.h = height,
|
||||
.internal_format = 0,
|
||||
};
|
||||
mpv_opengl_fbo mpv_fbo{};
|
||||
mpv_fbo.fbo = fbo;
|
||||
mpv_fbo.w = width;
|
||||
mpv_fbo.h = height;
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -353,7 +613,7 @@ void MpvPlayer::Command(const std::vector<std::string>& args) { CommandAsync(arg
|
||||
|
||||
void MpvPlayer::CommandAsync(const std::vector<std::string>& args, CommandCallback callback) {
|
||||
if (disposed_ || !mpv_) {
|
||||
if (callback) callback(0);
|
||||
if (callback) callback(MPV_ERROR_UNINITIALIZED);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -388,6 +648,16 @@ void MpvPlayer::SetPropertyAsync(const std::string& name, const std::string& val
|
||||
return;
|
||||
}
|
||||
|
||||
if (name == "pause" && !plezy::mpv_common::ParseEnabledFlag(value)) {
|
||||
auto completion = std::move(callback);
|
||||
callback = [this, completion = std::move(completion)](int error) {
|
||||
if (error >= 0 && !disposed_) {
|
||||
audio_recovery_.RequestResume();
|
||||
EnsureAudioRecoveryTimer();
|
||||
}
|
||||
if (completion) completion(error);
|
||||
};
|
||||
}
|
||||
uint64_t request_id = callback ? pending_requests_.RegisterStatus(std::move(callback)) : 0;
|
||||
|
||||
char* property_value = const_cast<char*>(value.c_str());
|
||||
@@ -400,7 +670,7 @@ void MpvPlayer::SetPropertyAsync(const std::string& name, const std::string& val
|
||||
|
||||
void MpvPlayer::GetPropertyAsync(const std::string& name, GetPropertyCallback callback) {
|
||||
if (disposed_ || !mpv_) {
|
||||
if (callback) callback(-1, "");
|
||||
if (callback) callback(MPV_ERROR_UNINITIALIZED, "");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -617,12 +887,16 @@ void MpvPlayer::LogRecovery(const std::string& text) {
|
||||
fl_value_unref(data);
|
||||
}
|
||||
|
||||
void MpvPlayer::TryAudioReload(const char* reason, int attempt) {
|
||||
void MpvPlayer::TryAudioReload(const char* reason, int attempt, uint64_t request_generation) {
|
||||
LogRecovery("issuing ao-reload (reason=" + std::string(reason) + ", attempt " + std::to_string(attempt) + ")");
|
||||
const std::string reason_copy = reason;
|
||||
CommandAsync({"ao-reload"}, [this, reason_copy, attempt](int error) {
|
||||
audio_recovery_.CompleteReload();
|
||||
LogRecovery(
|
||||
auto callback_context = callback_context_;
|
||||
CommandAsync({"ao-reload"}, [callback_context, reason_copy, attempt, request_generation](int error) {
|
||||
auto lease = callback_context->Acquire();
|
||||
if (!lease) return;
|
||||
MpvPlayer* player = lease.player();
|
||||
player->audio_recovery_.CompleteReload(request_generation);
|
||||
player->LogRecovery(
|
||||
"ao-reload completed (reason=" + reason_copy + ", attempt " + std::to_string(attempt) +
|
||||
", error=" + std::to_string(error) + ")");
|
||||
});
|
||||
@@ -634,7 +908,7 @@ void MpvPlayer::MaybeRunAudioRecovery() {
|
||||
return;
|
||||
}
|
||||
const char* reason = action.reason == plezy::mpv_common::AudioReloadReason::kResume ? "resume" : "null-fallback";
|
||||
TryAudioReload(reason, action.attempt);
|
||||
TryAudioReload(reason, action.attempt, action.request_generation);
|
||||
if (action.exhausted) {
|
||||
LogRecovery("audio recovery budget exhausted; waiting for device list change");
|
||||
}
|
||||
@@ -652,15 +926,7 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) {
|
||||
uint64_t request_id = event->reply_userdata;
|
||||
StatusCallback callback = pending_requests_.TakeStatus(request_id);
|
||||
if (callback) {
|
||||
int error = event->error;
|
||||
g_idle_add(
|
||||
[](gpointer data) -> gboolean {
|
||||
auto* pair = static_cast<std::pair<CommandCallback, int>*>(data);
|
||||
if (pair->first) pair->first(pair->second);
|
||||
delete pair;
|
||||
return G_SOURCE_REMOVE;
|
||||
},
|
||||
new std::pair<CommandCallback, int>(std::move(callback), error));
|
||||
callback(event->error);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -677,20 +943,13 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) {
|
||||
if (c_value) value = SanitizeUtf8(c_value);
|
||||
}
|
||||
}
|
||||
g_idle_add(
|
||||
[](gpointer data) -> gboolean {
|
||||
auto* tuple = static_cast<std::tuple<GetPropertyCallback, int, std::string>*>(data);
|
||||
const auto& callback = std::get<0>(*tuple);
|
||||
if (callback) callback(std::get<1>(*tuple), std::get<2>(*tuple));
|
||||
delete tuple;
|
||||
return G_SOURCE_REMOVE;
|
||||
},
|
||||
new std::tuple<GetPropertyCallback, int, std::string>(std::move(callback), error, std::move(value)));
|
||||
callback(error, value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MPV_EVENT_LOG_MESSAGE: {
|
||||
auto* msg = static_cast<mpv_event_log_message*>(event->data);
|
||||
if (!msg) break;
|
||||
g_message("MPV [%s] %s: %s", msg->level, msg->prefix, msg->text);
|
||||
|
||||
FlValue* data = fl_value_new_map();
|
||||
@@ -703,6 +962,7 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) {
|
||||
}
|
||||
case MPV_EVENT_PROPERTY_CHANGE: {
|
||||
auto* prop = static_cast<mpv_event_property*>(event->data);
|
||||
if (!prop || !prop->name) break;
|
||||
mpv_node node;
|
||||
node.format = prop->format;
|
||||
|
||||
@@ -722,6 +982,8 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) {
|
||||
case MPV_FORMAT_NODE:
|
||||
if (prop->data) {
|
||||
node = *static_cast<mpv_node*>(prop->data);
|
||||
} else {
|
||||
node.format = MPV_FORMAT_NONE;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -756,6 +1018,7 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) {
|
||||
case MPV_EVENT_END_FILE: {
|
||||
audio_recovery_.SetFileLoaded(false);
|
||||
auto* end = static_cast<mpv_event_end_file*>(event->data);
|
||||
if (!end) break;
|
||||
FlValue* data = fl_value_new_map();
|
||||
fl_value_set_string_take(data, "reason", fl_value_new_int(static_cast<int>(end->reason)));
|
||||
if (end->reason == MPV_END_FILE_REASON_ERROR) {
|
||||
@@ -773,6 +1036,7 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) {
|
||||
}
|
||||
case MPV_EVENT_FILE_LOADED: {
|
||||
audio_recovery_.SetFileLoaded(true);
|
||||
EnsureAudioRecoveryTimer();
|
||||
SendEvent("file-loaded");
|
||||
break;
|
||||
}
|
||||
@@ -784,13 +1048,37 @@ void MpvPlayer::HandleMpvEvent(mpv_event* event) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
FlValue* MpvPlayer::NodeToFlValue(mpv_node* node) {
|
||||
if (!node) return fl_value_new_null();
|
||||
NodeConversionBudget budget{
|
||||
/*remaining_entries=*/16384,
|
||||
/*remaining_bytes=*/16 * 1024 * 1024,
|
||||
};
|
||||
return NodeToFlValue(node, 0, &budget);
|
||||
}
|
||||
|
||||
bool MpvPlayer::ConvertNodeString(const char* input, NodeConversionBudget* budget, std::string* result) {
|
||||
if (!input || !budget || !result) return false;
|
||||
const size_t length = strnlen(input, budget->remaining_bytes + 1);
|
||||
if (length > budget->remaining_bytes) return false;
|
||||
budget->remaining_bytes -= length;
|
||||
*result = SanitizeUtf8(input, length);
|
||||
return true;
|
||||
}
|
||||
|
||||
FlValue* MpvPlayer::NodeToFlValue(mpv_node* node, size_t depth, NodeConversionBudget* budget) {
|
||||
constexpr size_t kMaxNodeDepth = 32;
|
||||
constexpr int kMaxNodeEntries = 16384;
|
||||
if (!node || !budget || depth >= kMaxNodeDepth || budget->remaining_entries == 0) {
|
||||
return fl_value_new_null();
|
||||
}
|
||||
--budget->remaining_entries;
|
||||
|
||||
switch (node->format) {
|
||||
case MPV_FORMAT_STRING:
|
||||
return fl_value_new_string(SanitizeUtf8(node->u.string).c_str());
|
||||
case MPV_FORMAT_STRING: {
|
||||
std::string value;
|
||||
if (!ConvertNodeString(node->u.string, budget, &value)) return fl_value_new_null();
|
||||
return fl_value_new_string(value.c_str());
|
||||
}
|
||||
case MPV_FORMAT_FLAG:
|
||||
return fl_value_new_bool(node->u.flag != 0);
|
||||
case MPV_FORMAT_INT64:
|
||||
@@ -798,18 +1086,35 @@ FlValue* MpvPlayer::NodeToFlValue(mpv_node* node) {
|
||||
case MPV_FORMAT_DOUBLE:
|
||||
return fl_value_new_float(node->u.double_);
|
||||
case MPV_FORMAT_NODE_ARRAY: {
|
||||
FlValue* list = fl_value_new_list();
|
||||
for (int i = 0; i < node->u.list->num; i++) {
|
||||
fl_value_append_take(list, NodeToFlValue(&node->u.list->values[i]));
|
||||
const mpv_node_list* list = node->u.list;
|
||||
if (!list || list->num < 0 || list->num > kMaxNodeEntries || (list->num > 0 && !list->values)) {
|
||||
return fl_value_new_null();
|
||||
}
|
||||
return list;
|
||||
FlValue* result = fl_value_new_list();
|
||||
for (int i = 0; i < list->num; i++) {
|
||||
fl_value_append_take(result, NodeToFlValue(&list->values[i], depth + 1, budget));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
case MPV_FORMAT_NODE_MAP: {
|
||||
FlValue* map = fl_value_new_map();
|
||||
for (int i = 0; i < node->u.list->num; i++) {
|
||||
fl_value_set_string_take(map, node->u.list->keys[i], NodeToFlValue(&node->u.list->values[i]));
|
||||
const mpv_node_list* map = node->u.list;
|
||||
if (!map || map->num < 0 || map->num > kMaxNodeEntries || (map->num > 0 && (!map->keys || !map->values))) {
|
||||
return fl_value_new_null();
|
||||
}
|
||||
return map;
|
||||
FlValue* result = fl_value_new_map();
|
||||
for (int i = 0; i < map->num; i++) {
|
||||
if (!map->keys[i]) {
|
||||
fl_value_unref(result);
|
||||
return fl_value_new_null();
|
||||
}
|
||||
std::string key;
|
||||
if (!ConvertNodeString(map->keys[i], budget, &key)) {
|
||||
fl_value_unref(result);
|
||||
return fl_value_new_null();
|
||||
}
|
||||
fl_value_set_string_take(result, key.c_str(), NodeToFlValue(&map->values[i], depth + 1, budget));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
default:
|
||||
return fl_value_new_null();
|
||||
@@ -830,10 +1135,12 @@ void MpvPlayer::SendPropertyChange(const char* name, mpv_node* data) {
|
||||
fl_value_append_take(list, fl_value_new_null());
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(callback_mutex_);
|
||||
if (event_callback_) {
|
||||
event_callback_(list);
|
||||
EventCallback callback;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(callback_mutex_);
|
||||
callback = event_callback_;
|
||||
}
|
||||
if (callback) callback(list);
|
||||
fl_value_unref(list);
|
||||
}
|
||||
|
||||
@@ -845,19 +1152,23 @@ void MpvPlayer::SendEvent(const std::string& name, FlValue* data) {
|
||||
fl_value_set_string_take(event_map, "data", fl_value_ref(data));
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(callback_mutex_);
|
||||
if (event_callback_) {
|
||||
event_callback_(event_map);
|
||||
EventCallback callback;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(callback_mutex_);
|
||||
callback = event_callback_;
|
||||
}
|
||||
if (callback) callback(event_map);
|
||||
fl_value_unref(event_map);
|
||||
}
|
||||
|
||||
void MpvPlayer::SetHDREnabled(bool enabled, StatusCallback callback) {
|
||||
hdr_enabled_ = enabled;
|
||||
if (!mpv_) {
|
||||
if (callback) callback(0);
|
||||
return;
|
||||
}
|
||||
SetPropertyAsync("target-colorspace-hint", plezy::mpv_common::TargetColorspaceHint(enabled), std::move(callback));
|
||||
SetPropertyAsync(
|
||||
"target-colorspace-hint", plezy::mpv_common::TargetColorspaceHint(enabled),
|
||||
[this, enabled, callback = std::move(callback)](int error) mutable {
|
||||
if (plezy::mpv_common::SetPropertyStatusSucceeded(error) && !disposed_) {
|
||||
hdr_enabled_ = enabled;
|
||||
}
|
||||
if (callback) callback(error);
|
||||
});
|
||||
}
|
||||
} // namespace mpv
|
||||
|
||||
@@ -32,6 +32,45 @@ using EventCallback = std::function<void(::_FlValue*)>;
|
||||
/// Callback for requesting a redraw (called from mpv render update thread).
|
||||
using RedrawCallback = std::function<void()>;
|
||||
|
||||
// Linux-runner-internal teardown boundary. A render context may only be
|
||||
// released while its EGL context is current; the batch retains the shared mpv
|
||||
// handle until every render/context pair has been safely released.
|
||||
struct NativeRenderTeardownResource {
|
||||
mpv_render_context* render = nullptr;
|
||||
EGLDisplay display = EGL_NO_DISPLAY;
|
||||
EGLContext context = EGL_NO_CONTEXT;
|
||||
};
|
||||
|
||||
struct NativeRenderTeardownBatch {
|
||||
std::vector<NativeRenderTeardownResource> resources;
|
||||
mpv_handle* handle = nullptr;
|
||||
std::shared_ptr<void> callback_keep_alive;
|
||||
};
|
||||
|
||||
struct NativeRenderTeardownOperations {
|
||||
std::function<bool(EGLDisplay, EGLContext)> make_current;
|
||||
std::function<bool(EGLDisplay)> release_current;
|
||||
std::function<bool(EGLDisplay, EGLContext)> destroy_context;
|
||||
std::function<void(mpv_render_context*)> free_render;
|
||||
std::function<void(mpv_handle*)> terminate_handle;
|
||||
};
|
||||
|
||||
// Attempts one teardown pass. Failed resources remain owned by |batch| for a
|
||||
// later retry, and |handle| is never terminated while any resource remains.
|
||||
bool TryReleaseNativeRenderTeardown(NativeRenderTeardownBatch& batch, const NativeRenderTeardownOperations& operations);
|
||||
|
||||
// Releases render contexts retained by a failed initialization attempt. A
|
||||
// false result must block another render-context creation on the same core.
|
||||
bool TryReleaseRetainedNativeRenderContexts(
|
||||
std::vector<NativeRenderTeardownResource>& resources, const NativeRenderTeardownOperations& operations);
|
||||
|
||||
#ifdef PLEZY_MPV_PLAYER_LIFECYCLE_TEST
|
||||
// Focused-test boundary for exercising the process-lifetime teardown queue
|
||||
// without invoking real EGL or libmpv resources.
|
||||
void ConfigureNativeRenderTeardownQueueForTesting(NativeRenderTeardownOperations operations);
|
||||
void EnqueueNativeRenderTeardownForTesting(NativeRenderTeardownBatch batch);
|
||||
#endif
|
||||
|
||||
/// Wrapper for libmpv that handles initialization, OpenGL rendering,
|
||||
/// commands, properties, and event dispatching.
|
||||
class MpvPlayer {
|
||||
@@ -55,26 +94,26 @@ class MpvPlayer {
|
||||
bool InitRenderContext();
|
||||
|
||||
/// Returns true if the render context has been created.
|
||||
bool HasRenderContext() const { return mpv_gl_ != nullptr; }
|
||||
bool HasRenderContext() const;
|
||||
|
||||
/// Returns the isolated EGL display used for mpv rendering.
|
||||
EGLDisplay GetEglDisplay() const { return egl_display_; }
|
||||
EGLDisplay GetEglDisplay() const;
|
||||
|
||||
/// Returns the isolated EGL context used for mpv rendering.
|
||||
EGLContext GetEglContext() const { return egl_context_; }
|
||||
EGLContext GetEglContext() const;
|
||||
|
||||
/// Disposes mpv and releases resources.
|
||||
void Dispose();
|
||||
|
||||
/// Returns true if mpv is initialized (has both mpv handle and render
|
||||
/// context; audio-only players never have a render context).
|
||||
bool IsInitialized() const { return mpv_ != nullptr && (audio_only_ || mpv_gl_ != nullptr); }
|
||||
bool IsInitialized() const;
|
||||
|
||||
/// Returns true if this player has been disposed.
|
||||
bool IsDisposed() const { return disposed_.load(); }
|
||||
|
||||
/// Returns true if mpv handle exists (even without render context).
|
||||
bool HasMpvHandle() const { return mpv_ != nullptr; }
|
||||
bool HasMpvHandle() const;
|
||||
|
||||
/// Queues an mpv command without waiting for completion.
|
||||
void Command(const std::vector<std::string>& args);
|
||||
@@ -120,6 +159,10 @@ class MpvPlayer {
|
||||
/// Sets the MPV log message level (e.g., "warn", "v", "debug").
|
||||
void SetLogLevel(const std::string& level);
|
||||
|
||||
/// Retries process-owned native teardown work on the managed EGL teardown
|
||||
/// thread. Primarily useful before creating another render context.
|
||||
static void RetryPendingNativeTeardown();
|
||||
|
||||
private:
|
||||
class CallbackContext {
|
||||
public:
|
||||
@@ -149,6 +192,7 @@ class MpvPlayer {
|
||||
|
||||
Lease Acquire();
|
||||
void DetachAndWait();
|
||||
void WaitUntilDetached();
|
||||
GMainContext* main_context() const { return main_context_; }
|
||||
|
||||
private:
|
||||
@@ -193,13 +237,20 @@ class MpvPlayer {
|
||||
/// Sends an event notification.
|
||||
void SendEvent(const std::string& name, ::_FlValue* data = nullptr);
|
||||
void MaybeRunAudioRecovery();
|
||||
void TryAudioReload(const char* reason, int attempt);
|
||||
void TryAudioReload(const char* reason, int attempt, uint64_t request_generation);
|
||||
void EnsureAudioRecoveryTimer();
|
||||
void LogRecovery(const std::string& text);
|
||||
void SetHDREnabled(bool enabled, StatusCallback callback = nullptr);
|
||||
|
||||
struct NodeConversionBudget {
|
||||
size_t remaining_entries;
|
||||
size_t remaining_bytes;
|
||||
};
|
||||
|
||||
/// Helper to convert mpv_node to FlValue.
|
||||
::_FlValue* NodeToFlValue(mpv_node* node);
|
||||
::_FlValue* NodeToFlValue(mpv_node* node, size_t depth, NodeConversionBudget* budget);
|
||||
bool ConvertNodeString(const char* input, NodeConversionBudget* budget, std::string* result);
|
||||
|
||||
const bool audio_only_;
|
||||
mpv_handle* mpv_ = nullptr;
|
||||
@@ -208,6 +259,8 @@ class MpvPlayer {
|
||||
// Isolated EGL context for mpv rendering (not shared with Flutter)
|
||||
EGLDisplay egl_display_ = EGL_NO_DISPLAY;
|
||||
EGLContext egl_context_ = EGL_NO_CONTEXT;
|
||||
std::vector<NativeRenderTeardownResource> retained_render_contexts_;
|
||||
mutable std::mutex native_mutex_;
|
||||
|
||||
std::atomic<bool> needs_redraw_{false};
|
||||
std::atomic<bool> disposed_{false};
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
#include <flutter_linux/flutter_linux.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <csignal>
|
||||
#include <cstdlib>
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
@@ -10,6 +18,56 @@
|
||||
#include <utility>
|
||||
|
||||
#include "mpv_player.h"
|
||||
#include "mpv_texture.h"
|
||||
|
||||
struct LifetimeTextureRegistrar {
|
||||
GObject parent_instance;
|
||||
FlTexture* texture;
|
||||
};
|
||||
|
||||
struct LifetimeTextureRegistrarClass {
|
||||
GObjectClass parent_class;
|
||||
};
|
||||
|
||||
static void LifetimeTextureRegistrarInterfaceInit(FlTextureRegistrarInterface* interface);
|
||||
static void LifetimeTextureRegistrarDispose(GObject* object);
|
||||
static void lifetime_texture_registrar_class_init(LifetimeTextureRegistrarClass* klass);
|
||||
static void lifetime_texture_registrar_init(LifetimeTextureRegistrar* self);
|
||||
|
||||
G_DEFINE_TYPE_WITH_CODE(
|
||||
LifetimeTextureRegistrar, lifetime_texture_registrar, G_TYPE_OBJECT,
|
||||
G_IMPLEMENT_INTERFACE(fl_texture_registrar_get_type(), LifetimeTextureRegistrarInterfaceInit))
|
||||
|
||||
static gboolean LifetimeTextureRegistrarRegister(FlTextureRegistrar* registrar, FlTexture* texture) {
|
||||
auto* self = reinterpret_cast<LifetimeTextureRegistrar*>(registrar);
|
||||
if (self->texture) return FALSE;
|
||||
self->texture = FL_TEXTURE(g_object_ref(texture));
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean LifetimeTextureRegistrarUnregister(FlTextureRegistrar* registrar, FlTexture* texture) {
|
||||
auto* self = reinterpret_cast<LifetimeTextureRegistrar*>(registrar);
|
||||
if (self->texture != texture) return FALSE;
|
||||
g_clear_object(&self->texture);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static void LifetimeTextureRegistrarInterfaceInit(FlTextureRegistrarInterface* interface) {
|
||||
interface->register_texture = LifetimeTextureRegistrarRegister;
|
||||
interface->unregister_texture = LifetimeTextureRegistrarUnregister;
|
||||
}
|
||||
|
||||
static void LifetimeTextureRegistrarDispose(GObject* object) {
|
||||
auto* self = reinterpret_cast<LifetimeTextureRegistrar*>(object);
|
||||
g_clear_object(&self->texture);
|
||||
G_OBJECT_CLASS(lifetime_texture_registrar_parent_class)->dispose(object);
|
||||
}
|
||||
|
||||
static void lifetime_texture_registrar_class_init(LifetimeTextureRegistrarClass* klass) {
|
||||
G_OBJECT_CLASS(klass)->dispose = LifetimeTextureRegistrarDispose;
|
||||
}
|
||||
|
||||
static void lifetime_texture_registrar_init(LifetimeTextureRegistrar* self) { self->texture = nullptr; }
|
||||
|
||||
namespace mpv {
|
||||
|
||||
@@ -27,6 +85,9 @@ class MpvPlayerLifecycleTestPeer {
|
||||
MpvPlayer::OnMpvRenderUpdate(context.get());
|
||||
}
|
||||
|
||||
static void WaitUntilDetached(const std::shared_ptr<MpvPlayer::CallbackContext>& context) {
|
||||
context->WaitUntilDetached();
|
||||
}
|
||||
static void ScheduleRecovery(MpvPlayer& player) { player.ScheduleRecoverySource(); }
|
||||
|
||||
static void RegisterPendingPropertyWrite(MpvPlayer& player, MpvPlayer::StatusCallback callback) {
|
||||
@@ -38,6 +99,16 @@ class MpvPlayerLifecycleTestPeer {
|
||||
return (player.wakeup_source_id_ != 0 ? 1 : 0) + (player.redraw_source_id_ != 0 ? 1 : 0) +
|
||||
(player.recovery_source_id_ != 0 ? 1 : 0);
|
||||
}
|
||||
static FlValue* ConvertNode(MpvPlayer& player, mpv_node* node) { return player.NodeToFlValue(node); }
|
||||
static FlValue* ConvertNodeWithBudget(
|
||||
MpvPlayer& player, mpv_node* node, size_t remaining_entries, size_t remaining_bytes) {
|
||||
MpvPlayer::NodeConversionBudget budget{remaining_entries, remaining_bytes};
|
||||
return player.NodeToFlValue(node, 0, &budget);
|
||||
}
|
||||
static void RegisterObservedNode(MpvPlayer& player, const std::string& name, int id) {
|
||||
player.observed_properties_.Register(name, "node", id);
|
||||
}
|
||||
static void HandleEvent(MpvPlayer& player, mpv_event* event) { player.HandleMpvEvent(event); }
|
||||
|
||||
static void HoldLease(
|
||||
const std::shared_ptr<MpvPlayer::CallbackContext>& context, std::mutex& mutex, std::condition_variable& condition,
|
||||
@@ -65,6 +136,291 @@ void Drain(GMainContext* context) {
|
||||
}
|
||||
}
|
||||
|
||||
bool WriteByte(int descriptor, char value) {
|
||||
for (;;) {
|
||||
const ssize_t written = write(descriptor, &value, 1);
|
||||
if (written == 1) return true;
|
||||
if (written < 0 && errno == EINTR) continue;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool ReadByte(int descriptor, char expected) {
|
||||
char value = '\0';
|
||||
for (;;) {
|
||||
const ssize_t received = read(descriptor, &value, 1);
|
||||
if (received == 1) return value == expected;
|
||||
if (received < 0 && errno == EINTR) continue;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
[[noreturn]] void ExitBlockedTeardownChild(int status) { _exit(status); }
|
||||
|
||||
int RunBlockedTeardownShutdownChild(int progress_read, int progress_write, int release_read) {
|
||||
auto* const completed_handle = reinterpret_cast<mpv_handle*>(0x11);
|
||||
auto* const blocked_render = reinterpret_cast<mpv_render_context*>(0x12);
|
||||
auto const blocked_display = reinterpret_cast<EGLDisplay>(0x13);
|
||||
auto const blocked_context = reinterpret_cast<EGLContext>(0x14);
|
||||
|
||||
NativeRenderTeardownOperations operations{
|
||||
[](EGLDisplay, EGLContext) { return true; },
|
||||
[](EGLDisplay) { return true; },
|
||||
[](EGLDisplay, EGLContext) { return true; },
|
||||
[progress_write, release_read, blocked_render](mpv_render_context* render) {
|
||||
if (render != blocked_render || !WriteByte(progress_write, 'B')) ExitBlockedTeardownChild(121);
|
||||
char release = '\0';
|
||||
for (;;) {
|
||||
const ssize_t received = read(release_read, &release, 1);
|
||||
if (received == 1) break;
|
||||
if (received < 0 && errno == EINTR) continue;
|
||||
ExitBlockedTeardownChild(122);
|
||||
}
|
||||
},
|
||||
[progress_write, completed_handle](mpv_handle* handle) {
|
||||
if (handle != completed_handle || !WriteByte(progress_write, 'R')) ExitBlockedTeardownChild(123);
|
||||
},
|
||||
};
|
||||
ConfigureNativeRenderTeardownQueueForTesting(std::move(operations));
|
||||
|
||||
NativeRenderTeardownBatch completed_batch;
|
||||
completed_batch.handle = completed_handle;
|
||||
EnqueueNativeRenderTeardownForTesting(std::move(completed_batch));
|
||||
if (!ReadByte(progress_read, 'R')) return 124;
|
||||
|
||||
NativeRenderTeardownBatch blocked_batch;
|
||||
blocked_batch.resources.push_back({blocked_render, blocked_display, blocked_context});
|
||||
EnqueueNativeRenderTeardownForTesting(std::move(blocked_batch));
|
||||
if (!ReadByte(progress_read, 'B')) return 125;
|
||||
|
||||
// Returning through std::exit below deliberately begins normal static
|
||||
// shutdown while the queue worker remains blocked in free_render.
|
||||
return 0;
|
||||
}
|
||||
|
||||
void TestProcessShutdownDoesNotJoinBlockedNativeTeardown() {
|
||||
int progress_pipe[2] = {-1, -1};
|
||||
int release_pipe[2] = {-1, -1};
|
||||
Check(pipe(progress_pipe) == 0, "could not create teardown progress barrier");
|
||||
if (pipe(release_pipe) != 0) {
|
||||
close(progress_pipe[0]);
|
||||
close(progress_pipe[1]);
|
||||
Check(false, "could not create teardown release barrier");
|
||||
}
|
||||
|
||||
const pid_t child = fork();
|
||||
if (child == 0) {
|
||||
close(release_pipe[1]);
|
||||
const int status = RunBlockedTeardownShutdownChild(progress_pipe[0], progress_pipe[1], release_pipe[0]);
|
||||
std::exit(status);
|
||||
}
|
||||
if (child < 0) {
|
||||
close(progress_pipe[0]);
|
||||
close(progress_pipe[1]);
|
||||
close(release_pipe[0]);
|
||||
close(release_pipe[1]);
|
||||
Check(false, "could not create teardown shutdown subprocess");
|
||||
}
|
||||
|
||||
close(progress_pipe[0]);
|
||||
close(progress_pipe[1]);
|
||||
close(release_pipe[0]);
|
||||
|
||||
std::mutex wait_mutex;
|
||||
std::condition_variable wait_condition;
|
||||
bool wait_finished = false;
|
||||
pid_t wait_result = -1;
|
||||
int child_status = 0;
|
||||
std::thread waiter([&]() {
|
||||
pid_t result;
|
||||
do {
|
||||
result = waitpid(child, &child_status, 0);
|
||||
} while (result < 0 && errno == EINTR);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(wait_mutex);
|
||||
wait_result = result;
|
||||
wait_finished = true;
|
||||
}
|
||||
wait_condition.notify_one();
|
||||
});
|
||||
|
||||
bool exited_before_deadline = false;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(wait_mutex);
|
||||
exited_before_deadline = wait_condition.wait_for(lock, std::chrono::seconds(2), [&]() { return wait_finished; });
|
||||
}
|
||||
if (!exited_before_deadline) kill(child, SIGKILL);
|
||||
close(release_pipe[1]);
|
||||
waiter.join();
|
||||
|
||||
Check(exited_before_deadline, "normal process shutdown joined a deliberately blocked native teardown");
|
||||
Check(wait_result == child, "could not collect teardown shutdown subprocess");
|
||||
Check(WIFEXITED(child_status), "teardown shutdown subprocess terminated abnormally");
|
||||
Check(WEXITSTATUS(child_status) == 0, "teardown shutdown subprocess did not reach normal static shutdown");
|
||||
}
|
||||
|
||||
struct TextureLifetimeState {
|
||||
std::mutex mutex;
|
||||
std::condition_variable condition;
|
||||
bool callback_entered = false;
|
||||
bool release_callback = false;
|
||||
std::atomic<bool> callback_finalized{false};
|
||||
bool finalized_during_callback = false;
|
||||
};
|
||||
|
||||
void BlockingTextureReadyCallback(gboolean, const gchar*, gpointer user_data) {
|
||||
auto* state = static_cast<TextureLifetimeState*>(user_data);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state->mutex);
|
||||
state->callback_entered = true;
|
||||
}
|
||||
state->condition.notify_all();
|
||||
|
||||
std::unique_lock<std::mutex> lock(state->mutex);
|
||||
state->condition.wait(lock, [state]() { return state->release_callback; });
|
||||
state->finalized_during_callback = state->callback_finalized.load();
|
||||
}
|
||||
|
||||
void TextureReadyCallbackFinalized(gpointer user_data) {
|
||||
static_cast<TextureLifetimeState*>(user_data)->callback_finalized = true;
|
||||
}
|
||||
|
||||
void TestPopulateRetainsTextureWhileBootstrapCallbackRuns() {
|
||||
auto* registrar = FL_TEXTURE_REGISTRAR(g_object_new(lifetime_texture_registrar_get_type(), nullptr));
|
||||
TextureLifetimeState state;
|
||||
MpvTexture* texture = mpv_texture_new(nullptr, registrar, nullptr);
|
||||
mpv_texture_set_ready_callback(texture, BlockingTextureReadyCallback, &state, TextureReadyCallbackFinalized);
|
||||
Check(
|
||||
fl_texture_registrar_register_texture(registrar, FL_TEXTURE(texture)),
|
||||
"the lifetime fixture must retain the registered texture");
|
||||
|
||||
gboolean populate_result = TRUE;
|
||||
GError* populate_error = nullptr;
|
||||
std::thread raster_thread([&]() {
|
||||
uint32_t target = 0;
|
||||
uint32_t name = 0;
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
auto* texture_class = FL_TEXTURE_GL_GET_CLASS(texture);
|
||||
populate_result = texture_class->populate(FL_TEXTURE_GL(texture), &target, &name, &width, &height, &populate_error);
|
||||
});
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(state.mutex);
|
||||
state.condition.wait(lock, [&state]() { return state.callback_entered; });
|
||||
}
|
||||
|
||||
// Match plugin teardown while populate is between releasing its mutex and
|
||||
// returning from the ready callback. Unregister drops the registrar's
|
||||
// reference before dispose drops the plugin's reference.
|
||||
Check(
|
||||
fl_texture_registrar_unregister_texture(registrar, FL_TEXTURE(texture)),
|
||||
"the lifetime fixture must unregister the texture");
|
||||
mpv_texture_dispose(texture);
|
||||
g_object_unref(texture);
|
||||
const bool finalized_before_populate_released = state.callback_finalized.load();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state.mutex);
|
||||
state.release_callback = true;
|
||||
}
|
||||
state.condition.notify_all();
|
||||
raster_thread.join();
|
||||
|
||||
Check(
|
||||
!finalized_before_populate_released,
|
||||
"platform disposal finalized the texture while its populate callback was still running");
|
||||
Check(
|
||||
!state.finalized_during_callback,
|
||||
"the ready callback was finalized before populate released its retained texture reference");
|
||||
Check(state.callback_finalized.load(), "the texture callback was not finalized after populate returned");
|
||||
Check(!populate_result, "a populate without a player must fail");
|
||||
Check(populate_error != nullptr, "failed populate must report an error");
|
||||
g_clear_error(&populate_error);
|
||||
g_object_unref(registrar);
|
||||
}
|
||||
|
||||
void TestNodeConversionRejectsMalformedPayloads() {
|
||||
MpvPlayer player;
|
||||
|
||||
mpv_node missing_list{};
|
||||
missing_list.format = MPV_FORMAT_NODE_ARRAY;
|
||||
missing_list.u.list = nullptr;
|
||||
FlValue* result = MpvPlayerLifecycleTestPeer::ConvertNode(player, &missing_list);
|
||||
Check(fl_value_get_type(result) == FL_VALUE_TYPE_NULL, "a node array without storage must decode as null");
|
||||
fl_value_unref(result);
|
||||
|
||||
mpv_node value{};
|
||||
value.format = MPV_FORMAT_INT64;
|
||||
value.u.int64 = 1;
|
||||
char* missing_key = nullptr;
|
||||
mpv_node_list malformed_map{1, &value, &missing_key};
|
||||
mpv_node map{};
|
||||
map.format = MPV_FORMAT_NODE_MAP;
|
||||
map.u.list = &malformed_map;
|
||||
result = MpvPlayerLifecycleTestPeer::ConvertNode(player, &map);
|
||||
Check(fl_value_get_type(result) == FL_VALUE_TYPE_NULL, "a node map with a null key must decode as null");
|
||||
fl_value_unref(result);
|
||||
|
||||
char invalid_utf8[] = {'a', static_cast<char>(0xFF), 'b', '\0'};
|
||||
mpv_node text{};
|
||||
text.format = MPV_FORMAT_STRING;
|
||||
text.u.string = invalid_utf8;
|
||||
result = MpvPlayerLifecycleTestPeer::ConvertNode(player, &text);
|
||||
Check(
|
||||
std::string(fl_value_get_string(result)) ==
|
||||
"a\xEF\xBF\xBD"
|
||||
"b",
|
||||
"invalid UTF-8 must be replaced before entering the Flutter codec");
|
||||
fl_value_unref(result);
|
||||
|
||||
char oversized_text[] = "bounded";
|
||||
text.u.string = oversized_text;
|
||||
result =
|
||||
MpvPlayerLifecycleTestPeer::ConvertNodeWithBudget(player, &text, /*remaining_entries=*/1, /*remaining_bytes=*/6);
|
||||
Check(fl_value_get_type(result) == FL_VALUE_TYPE_NULL, "a node string beyond the byte budget must decode as null");
|
||||
fl_value_unref(result);
|
||||
}
|
||||
|
||||
void TestNullNodePropertyPayloadDecodesAsNull() {
|
||||
MpvPlayer player;
|
||||
MpvPlayerLifecycleTestPeer::RegisterObservedNode(player, "track-list", 42);
|
||||
bool delivered = false;
|
||||
player.SetEventCallback([&delivered](FlValue* event) {
|
||||
Check(fl_value_get_type(event) == FL_VALUE_TYPE_LIST, "property event must remain a list");
|
||||
Check(fl_value_get_length(event) == 2, "property event must contain the ID and value");
|
||||
Check(fl_value_get_int(fl_value_get_list_value(event, 0)) == 42, "property event ID changed");
|
||||
Check(
|
||||
fl_value_get_type(fl_value_get_list_value(event, 1)) == FL_VALUE_TYPE_NULL,
|
||||
"a missing MPV node payload must decode as null");
|
||||
delivered = true;
|
||||
});
|
||||
|
||||
mpv_event_property property{};
|
||||
property.name = "track-list";
|
||||
property.format = MPV_FORMAT_NODE;
|
||||
property.data = nullptr;
|
||||
mpv_event event{};
|
||||
event.event_id = MPV_EVENT_PROPERTY_CHANGE;
|
||||
event.data = &property;
|
||||
MpvPlayerLifecycleTestPeer::HandleEvent(player, &event);
|
||||
Check(delivered, "null node property event was not delivered");
|
||||
}
|
||||
|
||||
void TestUnavailableCommandFails() {
|
||||
MpvPlayer player;
|
||||
int callback_count = 0;
|
||||
int status = MPV_ERROR_SUCCESS;
|
||||
|
||||
player.CommandAsync({"stop"}, [&](int error) {
|
||||
++callback_count;
|
||||
status = error;
|
||||
});
|
||||
|
||||
Check(callback_count == 1, "a command without an mpv handle must complete exactly once");
|
||||
Check(status == MPV_ERROR_UNINITIALIZED, "a command without an mpv handle must fail as uninitialized");
|
||||
}
|
||||
|
||||
void TestUnavailablePropertyWriteFails() {
|
||||
MpvPlayer player;
|
||||
int callback_count = 0;
|
||||
@@ -113,7 +469,6 @@ void TestQueuedSourcesAreRetired(GMainContext* context) {
|
||||
|
||||
MpvPlayerLifecycleTestPeer::Wakeup(callback_context);
|
||||
MpvPlayerLifecycleTestPeer::RenderUpdate(callback_context);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(125));
|
||||
Drain(context);
|
||||
Check(redraws == 0, "detached callbacks must not publish redraws");
|
||||
}
|
||||
@@ -138,7 +493,7 @@ void TestNativeLeaseBlocksDispose() {
|
||||
player->Dispose();
|
||||
disposed = true;
|
||||
});
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(25));
|
||||
MpvPlayerLifecycleTestPeer::WaitUntilDetached(callback_context);
|
||||
Check(!disposed.load(), "dispose returned while a native callback lease was active");
|
||||
|
||||
{
|
||||
@@ -206,6 +561,131 @@ void TestRapidReplacementCannotReceiveOldCallbacks(GMainContext* context) {
|
||||
}
|
||||
}
|
||||
|
||||
void TestRenderTeardownRetainsOwnershipUntilContextIsCurrent() {
|
||||
NativeRenderTeardownBatch batch;
|
||||
auto* render = reinterpret_cast<mpv_render_context*>(1);
|
||||
auto* handle = reinterpret_cast<mpv_handle*>(2);
|
||||
auto display = reinterpret_cast<EGLDisplay>(3);
|
||||
auto context = reinterpret_cast<EGLContext>(4);
|
||||
batch.resources.push_back({render, display, context});
|
||||
batch.handle = handle;
|
||||
|
||||
bool allow_make_current = false;
|
||||
bool allow_release = true;
|
||||
int make_current_calls = 0;
|
||||
int release_calls = 0;
|
||||
int free_calls = 0;
|
||||
int destroy_calls = 0;
|
||||
int terminate_calls = 0;
|
||||
NativeRenderTeardownOperations operations{
|
||||
[&](EGLDisplay actual_display, EGLContext actual_context) {
|
||||
Check(actual_display == display && actual_context == context, "teardown must bind the retained EGL context");
|
||||
++make_current_calls;
|
||||
return allow_make_current;
|
||||
},
|
||||
[&](EGLDisplay actual_display) {
|
||||
Check(actual_display == display, "teardown must release the retained EGL display");
|
||||
++release_calls;
|
||||
return allow_release;
|
||||
},
|
||||
[&](EGLDisplay actual_display, EGLContext actual_context) {
|
||||
Check(actual_display == display && actual_context == context, "teardown destroyed the wrong EGL context");
|
||||
++destroy_calls;
|
||||
return true;
|
||||
},
|
||||
[&](mpv_render_context* actual_render) {
|
||||
Check(actual_render == render, "teardown freed the wrong render context");
|
||||
++free_calls;
|
||||
},
|
||||
[&](mpv_handle* actual_handle) {
|
||||
Check(actual_handle == handle, "teardown terminated the wrong mpv handle");
|
||||
++terminate_calls;
|
||||
},
|
||||
};
|
||||
|
||||
Check(!TryReleaseNativeRenderTeardown(batch, operations), "a failed EGL bind must retain the native teardown batch");
|
||||
Check(make_current_calls == 1, "teardown must attempt to bind the required EGL context");
|
||||
Check(
|
||||
free_calls == 0 && release_calls == 0 && destroy_calls == 0 && terminate_calls == 0,
|
||||
"a failed EGL bind must not free, destroy, or terminate dependent native objects");
|
||||
Check(
|
||||
batch.resources.size() == 1 && batch.resources.front().render == render && batch.handle == handle,
|
||||
"a failed EGL bind must preserve complete ownership for retry");
|
||||
|
||||
allow_make_current = true;
|
||||
Check(TryReleaseNativeRenderTeardown(batch, operations), "a later valid EGL bind must complete retained teardown");
|
||||
Check(batch.resources.empty() && batch.handle == nullptr, "successful retry must consume the teardown batch");
|
||||
Check(
|
||||
free_calls == 1 && release_calls == 1 && destroy_calls == 1 && terminate_calls == 1,
|
||||
"successful retry must release the render, EGL context, and then the shared handle exactly once");
|
||||
}
|
||||
|
||||
void TestRenderTeardownDoesNotDestroyAStillCurrentContext() {
|
||||
NativeRenderTeardownBatch batch;
|
||||
auto* render = reinterpret_cast<mpv_render_context*>(5);
|
||||
auto* handle = reinterpret_cast<mpv_handle*>(6);
|
||||
auto display = reinterpret_cast<EGLDisplay>(7);
|
||||
auto context = reinterpret_cast<EGLContext>(8);
|
||||
batch.resources.push_back({render, display, context});
|
||||
batch.handle = handle;
|
||||
|
||||
bool allow_release = false;
|
||||
int free_calls = 0;
|
||||
int destroy_calls = 0;
|
||||
int terminate_calls = 0;
|
||||
NativeRenderTeardownOperations operations{
|
||||
[](EGLDisplay, EGLContext) { return true; },
|
||||
[&](EGLDisplay) { return allow_release; },
|
||||
[&](EGLDisplay, EGLContext) {
|
||||
++destroy_calls;
|
||||
return true;
|
||||
},
|
||||
[&](mpv_render_context*) { ++free_calls; },
|
||||
[&](mpv_handle*) { ++terminate_calls; },
|
||||
};
|
||||
|
||||
Check(!TryReleaseNativeRenderTeardown(batch, operations), "a context that cannot be released must remain queued");
|
||||
Check(free_calls == 1, "the render context may be freed only after its EGL context became current");
|
||||
Check(
|
||||
destroy_calls == 0 && terminate_calls == 0 && batch.resources.front().render == nullptr,
|
||||
"failed EGL release must retain the context and handle without double-freeing the render");
|
||||
|
||||
allow_release = true;
|
||||
Check(TryReleaseNativeRenderTeardown(batch, operations), "a later EGL release must finish teardown");
|
||||
Check(
|
||||
free_calls == 1 && destroy_calls == 1 && terminate_calls == 1,
|
||||
"retry must not repeat render-context destruction");
|
||||
}
|
||||
|
||||
void TestRetainedRenderBlocksAnotherCreationUntilReleased() {
|
||||
std::vector<NativeRenderTeardownResource> retained{
|
||||
{reinterpret_cast<mpv_render_context*>(9), reinterpret_cast<EGLDisplay>(10), reinterpret_cast<EGLContext>(11)}};
|
||||
bool allow_make_current = false;
|
||||
int free_calls = 0;
|
||||
int destroy_calls = 0;
|
||||
int render_creations = 0;
|
||||
NativeRenderTeardownOperations operations{
|
||||
[&](EGLDisplay, EGLContext) { return allow_make_current; },
|
||||
[](EGLDisplay) { return true; },
|
||||
[&](EGLDisplay, EGLContext) {
|
||||
++destroy_calls;
|
||||
return true;
|
||||
},
|
||||
[&](mpv_render_context*) { ++free_calls; },
|
||||
[](mpv_handle*) { Check(false, "retained initialization cleanup must not terminate the shared core"); },
|
||||
};
|
||||
|
||||
if (TryReleaseRetainedNativeRenderContexts(retained, operations)) ++render_creations;
|
||||
Check(render_creations == 0, "a retained render context must block another creation on the same core");
|
||||
Check(retained.size() == 1, "failed retained cleanup must preserve ownership for another GL-thread retry");
|
||||
|
||||
allow_make_current = true;
|
||||
if (TryReleaseRetainedNativeRenderContexts(retained, operations)) ++render_creations;
|
||||
Check(render_creations == 1, "render creation may resume after retained teardown completes");
|
||||
Check(retained.empty(), "successful retained teardown must consume the old render context");
|
||||
Check(free_calls == 1 && destroy_calls == 1, "retained teardown must release each native object exactly once");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mpv
|
||||
|
||||
@@ -214,12 +694,20 @@ int main() {
|
||||
g_main_context_push_thread_default(context);
|
||||
|
||||
try {
|
||||
mpv::TestProcessShutdownDoesNotJoinBlockedNativeTeardown();
|
||||
mpv::TestPopulateRetainsTextureWhileBootstrapCallbackRuns();
|
||||
mpv::TestUnavailablePropertyWriteFails();
|
||||
mpv::TestNodeConversionRejectsMalformedPayloads();
|
||||
mpv::TestUnavailableCommandFails();
|
||||
mpv::TestPendingPropertyWriteFailsOnDispose();
|
||||
mpv::TestQueuedSourcesAreRetired(context);
|
||||
mpv::TestNativeLeaseBlocksDispose();
|
||||
mpv::TestWakeupAndRedrawCoalesce(context);
|
||||
mpv::TestRapidReplacementCannotReceiveOldCallbacks(context);
|
||||
mpv::TestRenderTeardownRetainsOwnershipUntilContextIsCurrent();
|
||||
mpv::TestRenderTeardownDoesNotDestroyAStillCurrentContext();
|
||||
mpv::TestNullNodePropertyPayloadDecodesAsNull();
|
||||
mpv::TestRetainedRenderBlocksAnotherCreationUntilReleased();
|
||||
} catch (const std::exception& error) {
|
||||
g_main_context_pop_thread_default(context);
|
||||
g_main_context_unref(context);
|
||||
|
||||
+193
-56
@@ -1,9 +1,13 @@
|
||||
#include "mpv_plugin.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
|
||||
#include "mpv_texture.h"
|
||||
|
||||
enum class VideoBootstrapState { kIdle, kPending, kReady, kFailed };
|
||||
using PlayerPtr = std::unique_ptr<mpv::MpvPlayer>;
|
||||
|
||||
struct _MpvPlugin {
|
||||
GObject parent_instance;
|
||||
|
||||
@@ -12,11 +16,17 @@ struct _MpvPlugin {
|
||||
FlEventChannel* event_channel;
|
||||
FlTextureRegistrar* texture_registrar;
|
||||
|
||||
std::unique_ptr<mpv::MpvPlayer> player;
|
||||
PlayerPtr player;
|
||||
MpvTexture* texture; // owned via GObject ref
|
||||
gboolean texture_registered;
|
||||
gboolean visible;
|
||||
gboolean initialized;
|
||||
gboolean audio_only;
|
||||
VideoBootstrapState bootstrap_state;
|
||||
gchar* bootstrap_error;
|
||||
FlMethodCall* ready_call;
|
||||
guint64 generation;
|
||||
guint ready_timeout_source_id;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(MpvPlugin, mpv_plugin, G_TYPE_OBJECT)
|
||||
@@ -27,48 +37,163 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel, FlMethodCall
|
||||
static void send_event(MpvPlugin* self, FlValue* event) {
|
||||
if (self->event_channel) {
|
||||
g_autoptr(GError) error = nullptr;
|
||||
if (!fl_event_channel_send(self->event_channel, event, nullptr, &error)) {
|
||||
if (error != nullptr) {
|
||||
g_warning("Failed to send event: %s", error->message);
|
||||
}
|
||||
if (!fl_event_channel_send(self->event_channel, event, nullptr, &error) && error != nullptr) {
|
||||
g_warning("Failed to send event: %s", error->message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void mpv_plugin_dispose(GObject* object) {
|
||||
MpvPlugin* self = MPV_PLUGIN(object);
|
||||
static gboolean handle_ready_timeout(gpointer user_data);
|
||||
|
||||
// Texture must be disposed BEFORE player — mpv_texture_dispose needs
|
||||
// the player's EGL context to clean up GL resources.
|
||||
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;
|
||||
}
|
||||
|
||||
static void release_video_resources(MpvPlugin* self) {
|
||||
++self->generation;
|
||||
if (self->player) {
|
||||
// The texture is a raw callback target. Revoke both callback paths before
|
||||
// unregistering or unreferencing it; Dispose then drains any callback
|
||||
// already holding a native lease.
|
||||
self->player->SetRedrawCallback(nullptr);
|
||||
self->player->SetEventCallback(nullptr);
|
||||
}
|
||||
if (self->texture) {
|
||||
mpv_texture_dispose(self->texture);
|
||||
if (self->texture_registrar) {
|
||||
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();
|
||||
}
|
||||
self->initialized = FALSE;
|
||||
self->visible = FALSE;
|
||||
}
|
||||
|
||||
struct TextureReadyContext {
|
||||
MpvPlugin* plugin;
|
||||
guint64 generation;
|
||||
};
|
||||
|
||||
struct TextureReadyResult {
|
||||
MpvPlugin* plugin;
|
||||
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 mpv_plugin_dispose(GObject* object) {
|
||||
MpvPlugin* self = MPV_PLUGIN(object);
|
||||
complete_ready_call(self, FALSE, "Video initialization was cancelled");
|
||||
release_video_resources(self);
|
||||
self->bootstrap_state = VideoBootstrapState::kIdle;
|
||||
g_clear_pointer(&self->bootstrap_error, g_free);
|
||||
g_clear_object(&self->method_channel);
|
||||
g_clear_object(&self->event_channel);
|
||||
g_clear_object(&self->registrar);
|
||||
|
||||
G_OBJECT_CLASS(mpv_plugin_parent_class)->dispose(object);
|
||||
}
|
||||
|
||||
static void mpv_plugin_class_init(MpvPluginClass* klass) { G_OBJECT_CLASS(klass)->dispose = mpv_plugin_dispose; }
|
||||
static void mpv_plugin_finalize(GObject* object) {
|
||||
MpvPlugin* self = MPV_PLUGIN(object);
|
||||
self->player.~PlayerPtr();
|
||||
G_OBJECT_CLASS(mpv_plugin_parent_class)->finalize(object);
|
||||
}
|
||||
|
||||
static void mpv_plugin_class_init(MpvPluginClass* klass) {
|
||||
G_OBJECT_CLASS(klass)->dispose = mpv_plugin_dispose;
|
||||
G_OBJECT_CLASS(klass)->finalize = mpv_plugin_finalize;
|
||||
}
|
||||
|
||||
static void mpv_plugin_init(MpvPlugin* self) {
|
||||
new (&self->player) PlayerPtr();
|
||||
self->visible = FALSE;
|
||||
self->initialized = FALSE;
|
||||
self->texture = nullptr;
|
||||
self->texture_registered = FALSE;
|
||||
self->texture_registrar = nullptr;
|
||||
self->audio_only = FALSE;
|
||||
self->bootstrap_state = VideoBootstrapState::kIdle;
|
||||
self->bootstrap_error = nullptr;
|
||||
self->ready_call = nullptr;
|
||||
self->generation = 0;
|
||||
self->ready_timeout_source_id = 0;
|
||||
}
|
||||
|
||||
MpvPlugin* mpv_plugin_new(FlPluginRegistrar* registrar, const gchar* channel_name, gboolean audio_only) {
|
||||
@@ -135,61 +260,73 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel, FlMethodCall
|
||||
response =
|
||||
FL_METHOD_RESPONSE(fl_method_error_response_new("INIT_FAILED", "Failed to initialize MPV player", nullptr));
|
||||
}
|
||||
} else if (self->initialized && self->texture) {
|
||||
// Already initialized — return existing texture ID
|
||||
} 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 {
|
||||
// Create player if it was disposed or doesn't exist
|
||||
g_clear_pointer(&self->bootstrap_error, g_free);
|
||||
self->bootstrap_state = VideoBootstrapState::kIdle;
|
||||
if (!self->player || self->player->IsDisposed()) {
|
||||
self->player = std::make_unique<mpv::MpvPlayer>();
|
||||
}
|
||||
|
||||
if (self->player->Initialize()) {
|
||||
// Create the FlTextureGL and register it
|
||||
if (!self->player->Initialize()) {
|
||||
release_video_resources(self);
|
||||
self->bootstrap_state = VideoBootstrapState::kFailed;
|
||||
self->bootstrap_error = g_strdup("Failed to initialize MPV player");
|
||||
response = FL_METHOD_RESPONSE(fl_method_error_response_new("INIT_FAILED", self->bootstrap_error, nullptr));
|
||||
} else {
|
||||
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);
|
||||
|
||||
fl_texture_registrar_register_texture(self->texture_registrar, FL_TEXTURE(self->texture));
|
||||
|
||||
// Create the render context eagerly — mpv needs it BEFORE any
|
||||
// file is loaded, otherwise VO init fails with "No render context
|
||||
// set" and the video track is dropped entirely.
|
||||
self->player->InitRenderContext();
|
||||
|
||||
// Set redraw callback: when mpv has a frame, mark texture available
|
||||
MpvTexture* tex = self->texture;
|
||||
self->player->SetRedrawCallback([tex]() { mpv_texture_mark_frame_available(tex); });
|
||||
|
||||
self->initialized = TRUE;
|
||||
|
||||
// Set up event callback
|
||||
self->player->SetEventCallback([self](FlValue* event) { send_event(self, event); });
|
||||
|
||||
// Return the texture ID for the Dart Texture widget
|
||||
response =
|
||||
FL_METHOD_RESPONSE(fl_method_success_response_new(fl_value_new_int(mpv_texture_get_id(self->texture))));
|
||||
} else {
|
||||
response =
|
||||
FL_METHOD_RESPONSE(fl_method_error_response_new("INIT_FAILED", "Failed to initialize MPV player", nullptr));
|
||||
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 if (strcmp(method, "waitForVideoReady") == 0) {
|
||||
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, "dispose") == 0) {
|
||||
// Disconnect and unregister texture FIRST — this stops Flutter from
|
||||
// calling populate(), preventing concurrent mpv_render_context_render()
|
||||
// during player disposal.
|
||||
if (self->texture) {
|
||||
mpv_texture_dispose(self->texture);
|
||||
fl_texture_registrar_unregister_texture(self->texture_registrar, FL_TEXTURE(self->texture));
|
||||
g_object_unref(self->texture);
|
||||
self->texture = nullptr;
|
||||
}
|
||||
if (self->player) {
|
||||
self->player->Dispose();
|
||||
self->player.reset();
|
||||
}
|
||||
self->initialized = FALSE;
|
||||
self->visible = FALSE;
|
||||
complete_ready_call(self, FALSE, "Video initialization was cancelled");
|
||||
release_video_resources(self);
|
||||
self->bootstrap_state = VideoBootstrapState::kIdle;
|
||||
g_clear_pointer(&self->bootstrap_error, g_free);
|
||||
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||
} else if (strcmp(method, "command") == 0) {
|
||||
if (!self->player || !self->initialized) {
|
||||
|
||||
+465
-186
@@ -3,199 +3,467 @@
|
||||
#include <epoxy/egl.h>
|
||||
#include <epoxy/gl.h>
|
||||
|
||||
// EGLImage extension function pointers
|
||||
typedef EGLImageKHR (*PFNEGLCREATEIMAGEKHRPROC)(EGLDisplay, EGLContext, EGLenum, EGLClientBuffer, const EGLint*);
|
||||
typedef EGLBoolean (*PFNEGLDESTROYIMAGEKHRPROC)(EGLDisplay, EGLImageKHR);
|
||||
typedef void (*PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)(GLenum, GLeglImageOES);
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
static PFNEGLCREATEIMAGEKHRPROC _eglCreateImageKHR = nullptr;
|
||||
static PFNEGLDESTROYIMAGEKHRPROC _eglDestroyImageKHR = nullptr;
|
||||
static PFNGLEGLIMAGETARGETTEXTURE2DOESPROC _glEGLImageTargetTexture2DOES = nullptr;
|
||||
#include "mpv_gpu_bootstrap.h"
|
||||
|
||||
static void init_egl_image_extensions() {
|
||||
static bool initialized = false;
|
||||
if (!initialized) {
|
||||
_eglCreateImageKHR = (PFNEGLCREATEIMAGEKHRPROC)eglGetProcAddress("eglCreateImageKHR");
|
||||
_eglDestroyImageKHR = (PFNEGLDESTROYIMAGEKHRPROC)eglGetProcAddress("eglDestroyImageKHR");
|
||||
_glEGLImageTargetTexture2DOES =
|
||||
(PFNGLEGLIMAGETARGETTEXTURE2DOESPROC)eglGetProcAddress("glEGLImageTargetTexture2DOES");
|
||||
initialized = true;
|
||||
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; // not owned
|
||||
FlTextureRegistrar* registrar; // not owned
|
||||
FlView* view; // not owned, for querying allocation size
|
||||
mpv::MpvPlayer* player;
|
||||
FlTextureRegistrar* registrar;
|
||||
FlView* view;
|
||||
|
||||
// mpv's FBO and texture (owned by mpv's isolated EGL context)
|
||||
GLuint mpv_fbo;
|
||||
GLuint mpv_texture;
|
||||
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;
|
||||
|
||||
// Flutter's texture (owned by Flutter's EGL context)
|
||||
GLuint flutter_texture;
|
||||
|
||||
// EGLImage bridging the two contexts
|
||||
EGLImageKHR egl_image;
|
||||
|
||||
int32_t width;
|
||||
int32_t height;
|
||||
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())
|
||||
|
||||
// Create/resize the FBO in mpv's context and the shared EGLImage + Flutter texture.
|
||||
static void ensure_textures(MpvTexture* self, int32_t w, int32_t h) {
|
||||
if (self->mpv_fbo != 0 && self->width == w && self->height == h) {
|
||||
return;
|
||||
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;
|
||||
}
|
||||
|
||||
EGLDisplay egl_display = self->player->GetEglDisplay();
|
||||
EGLContext egl_context = self->player->GetEglContext();
|
||||
|
||||
// Save Flutter's current EGL state
|
||||
EGLDisplay flutter_display = eglGetCurrentDisplay();
|
||||
EGLContext flutter_context = eglGetCurrentContext();
|
||||
EGLSurface flutter_draw = eglGetCurrentSurface(EGL_DRAW);
|
||||
EGLSurface flutter_read = eglGetCurrentSurface(EGL_READ);
|
||||
|
||||
// --- Switch to mpv's isolated context ---
|
||||
eglMakeCurrent(egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, egl_context);
|
||||
|
||||
// Clean up previous mpv resources
|
||||
if (self->mpv_texture != 0) {
|
||||
glDeleteTextures(1, &self->mpv_texture);
|
||||
}
|
||||
if (self->mpv_fbo != 0) {
|
||||
glDeleteFramebuffers(1, &self->mpv_fbo);
|
||||
}
|
||||
if (self->egl_image != EGL_NO_IMAGE_KHR) {
|
||||
_eglDestroyImageKHR(egl_display, self->egl_image);
|
||||
}
|
||||
|
||||
self->width = w;
|
||||
self->height = h;
|
||||
|
||||
// Create mpv's texture and 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, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||
|
||||
glGenFramebuffers(1, &self->mpv_fbo);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, self->mpv_fbo);
|
||||
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, self->mpv_texture, 0);
|
||||
|
||||
// Create EGLImage from mpv's texture for cross-context sharing
|
||||
EGLint image_attribs[] = {EGL_NONE};
|
||||
self->egl_image = _eglCreateImageKHR(
|
||||
egl_display, egl_context, EGL_GL_TEXTURE_2D_KHR, (EGLClientBuffer)(uintptr_t)self->mpv_texture, image_attribs);
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glFlush();
|
||||
|
||||
// --- Switch back to Flutter's context ---
|
||||
eglMakeCurrent(flutter_display, flutter_draw, flutter_read, flutter_context);
|
||||
|
||||
// Clean up previous Flutter texture
|
||||
if (self->flutter_texture != 0) {
|
||||
glDeleteTextures(1, &self->flutter_texture);
|
||||
}
|
||||
|
||||
// Create Flutter's texture backed by the EGLImage
|
||||
glGenTextures(1, &self->flutter_texture);
|
||||
glBindTexture(GL_TEXTURE_2D, self->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);
|
||||
_glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, self->egl_image);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
g_mutex_unlock(&self->bootstrap_mutex);
|
||||
if (callback) callback(success, message, user_data);
|
||||
}
|
||||
|
||||
static gboolean mpv_texture_populate(
|
||||
FlTextureGL* gl_texture, uint32_t* target, uint32_t* name, uint32_t* width, uint32_t* height, GError** error) {
|
||||
MpvTexture* self = MPV_TEXTURE(gl_texture);
|
||||
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");
|
||||
}
|
||||
|
||||
if (!self->player) {
|
||||
return FALSE;
|
||||
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());
|
||||
}
|
||||
|
||||
// Lazily create the mpv render context on first populate() call,
|
||||
// since Flutter's GL context is current here.
|
||||
if (!self->player->HasRenderContext()) {
|
||||
if (!self->player->InitRenderContext()) {
|
||||
g_set_error(error, g_quark_from_static_string("mpv"), 0, "Failed to create mpv render context");
|
||||
return FALSE;
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
// Determine target size from the FlView widget allocation.
|
||||
GtkAllocation alloc;
|
||||
gtk_widget_get_allocation(GTK_WIDGET(self->view), &alloc);
|
||||
int scale = gtk_widget_get_scale_factor(GTK_WIDGET(self->view));
|
||||
int32_t w = alloc.width * scale;
|
||||
int32_t h = alloc.height * scale;
|
||||
if (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 (w <= 0 || h <= 0) {
|
||||
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;
|
||||
}
|
||||
|
||||
ensure_textures(self, w, h);
|
||||
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");
|
||||
}
|
||||
|
||||
// Save Flutter's current EGL state
|
||||
EGLDisplay flutter_display = eglGetCurrentDisplay();
|
||||
EGLContext flutter_context = eglGetCurrentContext();
|
||||
EGLSurface flutter_draw = eglGetCurrentSurface(EGL_DRAW);
|
||||
EGLSurface flutter_read = eglGetCurrentSurface(EGL_READ);
|
||||
|
||||
// Switch to mpv's isolated context for rendering
|
||||
EGLDisplay egl_display = self->player->GetEglDisplay();
|
||||
EGLContext egl_context = self->player->GetEglContext();
|
||||
eglMakeCurrent(egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, egl_context);
|
||||
|
||||
// Render mpv into its FBO
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, self->mpv_fbo);
|
||||
ClearGlErrors();
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, self->active->mpv_fbo);
|
||||
self->player->ClearRedrawFlag();
|
||||
self->player->Render(w, h, static_cast<int>(self->mpv_fbo));
|
||||
self->player->Render(requested_width, requested_height, static_cast<int>(self->active->mpv_fbo));
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
glFlush();
|
||||
|
||||
// Restore Flutter's context
|
||||
eglMakeCurrent(flutter_display, flutter_draw, flutter_read, flutter_context);
|
||||
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->flutter_texture;
|
||||
*width = static_cast<uint32_t>(w);
|
||||
*height = static_cast<uint32_t>(h);
|
||||
|
||||
*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 = mpv_texture_populate;
|
||||
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;
|
||||
self->mpv_fbo = 0;
|
||||
self->mpv_texture = 0;
|
||||
self->flutter_texture = 0;
|
||||
self->egl_image = EGL_NO_IMAGE_KHR;
|
||||
self->width = 0;
|
||||
self->height = 0;
|
||||
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) {
|
||||
init_egl_image_extensions();
|
||||
MpvTexture* self = MPV_TEXTURE(g_object_new(MPV_TEXTURE_TYPE, nullptr));
|
||||
self->player = player;
|
||||
self->registrar = registrar;
|
||||
@@ -203,59 +471,70 @@ MpvTexture* mpv_texture_new(mpv::MpvPlayer* player, FlTextureRegistrar* registra
|
||||
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 && self->registrar) {
|
||||
fl_texture_registrar_mark_texture_frame_available(self->registrar, FL_TEXTURE(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;
|
||||
|
||||
EGLDisplay egl_display = EGL_NO_DISPLAY;
|
||||
EGLContext egl_context = EGL_NO_CONTEXT;
|
||||
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) {
|
||||
egl_display = self->player->GetEglDisplay();
|
||||
egl_context = self->player->GetEglContext();
|
||||
}
|
||||
|
||||
// Clean up Flutter's texture (in Flutter's current context)
|
||||
if (self->flutter_texture != 0) {
|
||||
glDeleteTextures(1, &self->flutter_texture);
|
||||
self->flutter_texture = 0;
|
||||
}
|
||||
|
||||
// Clean up EGLImage
|
||||
if (self->egl_image != EGL_NO_IMAGE_KHR && egl_display != EGL_NO_DISPLAY) {
|
||||
_eglDestroyImageKHR(egl_display, self->egl_image);
|
||||
self->egl_image = EGL_NO_IMAGE_KHR;
|
||||
}
|
||||
|
||||
// Clean up mpv's GL resources in mpv's context
|
||||
if (egl_context != EGL_NO_CONTEXT) {
|
||||
EGLDisplay cur_display = eglGetCurrentDisplay();
|
||||
EGLContext cur_context = eglGetCurrentContext();
|
||||
EGLSurface cur_draw = eglGetCurrentSurface(EGL_DRAW);
|
||||
EGLSurface cur_read = eglGetCurrentSurface(EGL_READ);
|
||||
|
||||
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;
|
||||
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);
|
||||
}
|
||||
if (self->mpv_fbo != 0) {
|
||||
glDeleteFramebuffers(1, &self->mpv_fbo);
|
||||
self->mpv_fbo = 0;
|
||||
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);
|
||||
}
|
||||
|
||||
eglMakeCurrent(cur_display, cur_draw, cur_read, cur_context);
|
||||
}
|
||||
|
||||
*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)); }
|
||||
|
||||
@@ -14,6 +14,12 @@ 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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user