fix(runtime): harden application service boundaries
This commit is contained in:
@@ -44,3 +44,53 @@ 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")
|
||||
|
||||
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_player_lifecycle_test.cc"
|
||||
)
|
||||
apply_standard_settings(mpv_player_lifecycle_test)
|
||||
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)
|
||||
target_link_libraries(mpv_player_lifecycle_test PRIVATE PkgConfig::EPOXY)
|
||||
target_link_libraries(mpv_player_lifecycle_test PRIVATE simdutf)
|
||||
target_link_libraries(mpv_player_lifecycle_test PRIVATE Threads::Threads)
|
||||
target_include_directories(mpv_player_lifecycle_test PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||
target_include_directories(mpv_player_lifecycle_test PRIVATE "${CMAKE_SOURCE_DIR}/../shared/cpp")
|
||||
|
||||
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)
|
||||
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)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test(NAME mpv_player_lifecycle_test COMMAND mpv_player_lifecycle_test)
|
||||
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)
|
||||
|
||||
add_executable(mpv_property_result_contract_test
|
||||
"../../shared/mpv/mpv_player_common_test.cpp"
|
||||
)
|
||||
apply_standard_settings(mpv_property_result_contract_test)
|
||||
target_link_libraries(mpv_property_result_contract_test PRIVATE PkgConfig::MPV Threads::Threads)
|
||||
target_include_directories(mpv_property_result_contract_test PRIVATE "../../shared/mpv")
|
||||
|
||||
add_test(NAME mpv_property_result_contract_test COMMAND mpv_property_result_contract_test)
|
||||
endif()
|
||||
|
||||
+224
-84
@@ -22,7 +22,68 @@ static void* get_opengl_proc_address(void* ctx, const char* name) {
|
||||
|
||||
namespace mpv {
|
||||
|
||||
MpvPlayer::MpvPlayer(bool audio_only) : audio_only_(audio_only) {}
|
||||
MpvPlayer::CallbackContext::Lease::Lease(CallbackContext* context, MpvPlayer* player)
|
||||
: context_(context), player_(player) {}
|
||||
|
||||
MpvPlayer::CallbackContext::Lease::Lease(Lease&& other) noexcept : context_(other.context_), player_(other.player_) {
|
||||
other.context_ = nullptr;
|
||||
other.player_ = nullptr;
|
||||
}
|
||||
|
||||
MpvPlayer::CallbackContext::Lease& MpvPlayer::CallbackContext::Lease::operator=(Lease&& other) noexcept {
|
||||
if (this != &other) {
|
||||
Release();
|
||||
context_ = other.context_;
|
||||
player_ = other.player_;
|
||||
other.context_ = nullptr;
|
||||
other.player_ = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
MpvPlayer::CallbackContext::Lease::~Lease() { Release(); }
|
||||
|
||||
void MpvPlayer::CallbackContext::Lease::Release() {
|
||||
if (!context_) return;
|
||||
context_->ReleaseLease();
|
||||
context_ = nullptr;
|
||||
player_ = nullptr;
|
||||
}
|
||||
|
||||
MpvPlayer::CallbackContext::CallbackContext(MpvPlayer* player)
|
||||
: player_(player), main_context_(g_main_context_ref_thread_default()) {}
|
||||
|
||||
MpvPlayer::CallbackContext::~CallbackContext() { g_main_context_unref(main_context_); }
|
||||
|
||||
MpvPlayer::CallbackContext::Lease MpvPlayer::CallbackContext::Acquire() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!player_) return Lease();
|
||||
++in_flight_;
|
||||
return Lease(this, player_);
|
||||
}
|
||||
|
||||
void MpvPlayer::CallbackContext::DetachAndWait() {
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
player_ = nullptr;
|
||||
quiescent_.wait(lock, [this]() { return in_flight_ == 0; });
|
||||
}
|
||||
|
||||
void MpvPlayer::CallbackContext::ReleaseLease() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
--in_flight_;
|
||||
if (in_flight_ == 0) quiescent_.notify_all();
|
||||
}
|
||||
|
||||
struct MpvPlayer::SourceCallbackData {
|
||||
explicit SourceCallbackData(std::shared_ptr<CallbackContext> callback_context)
|
||||
: context(std::move(callback_context)) {}
|
||||
|
||||
std::shared_ptr<CallbackContext> context;
|
||||
guint source_id = 0;
|
||||
};
|
||||
|
||||
MpvPlayer::MpvPlayer(bool audio_only)
|
||||
: audio_only_(audio_only), callback_context_(std::make_shared<CallbackContext>(this)) {}
|
||||
|
||||
MpvPlayer::~MpvPlayer() { Dispose(); }
|
||||
|
||||
@@ -82,7 +143,7 @@ bool MpvPlayer::Initialize() {
|
||||
}
|
||||
|
||||
// Set up event wakeup callback.
|
||||
mpv_set_wakeup_callback(mpv_, OnMpvWakeup, this);
|
||||
mpv_set_wakeup_callback(mpv_, OnMpvWakeup, callback_context_.get());
|
||||
mpv_observe_property(mpv_, 0, "current-ao", MPV_FORMAT_STRING);
|
||||
mpv_observe_property(mpv_, 0, "audio-device-list", MPV_FORMAT_NONE);
|
||||
|
||||
@@ -195,34 +256,33 @@ bool MpvPlayer::InitRenderContext() {
|
||||
}
|
||||
|
||||
// Set up render update callback.
|
||||
mpv_render_context_set_update_callback(mpv_gl_, OnMpvRenderUpdate, this);
|
||||
mpv_render_context_set_update_callback(mpv_gl_, OnMpvRenderUpdate, callback_context_.get());
|
||||
|
||||
g_message("MPV: Render context created with isolated EGL context");
|
||||
return true;
|
||||
}
|
||||
|
||||
void MpvPlayer::Dispose() {
|
||||
// 1. Set disposed flag atomically FIRST — all callback paths check this
|
||||
if (disposed_.exchange(true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Clear mpv's native callbacks to prevent new ones from firing
|
||||
// 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);
|
||||
}
|
||||
callback_context_->DetachAndWait();
|
||||
|
||||
// 3. Briefly hold mutex to null our callbacks
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(callback_mutex_);
|
||||
redraw_callback_ = nullptr;
|
||||
event_callback_ = nullptr;
|
||||
}
|
||||
|
||||
// 4. Cancel pending async requests.
|
||||
auto cancelled = pending_requests_.CancelAll();
|
||||
for (auto& callback : cancelled.status) {
|
||||
callback(-1);
|
||||
@@ -231,45 +291,39 @@ void MpvPlayer::Dispose() {
|
||||
callback(-1, "");
|
||||
}
|
||||
|
||||
// 5. Remove pending idle callbacks
|
||||
if (event_source_id_ != 0) {
|
||||
g_source_remove(event_source_id_);
|
||||
event_source_id_ = 0;
|
||||
}
|
||||
if (recovery_source_id_ != 0) {
|
||||
g_source_remove(recovery_source_id_);
|
||||
recovery_source_id_ = 0;
|
||||
}
|
||||
RemoveTrackedSources();
|
||||
|
||||
// 6. Free render context and mpv handle in a background thread.
|
||||
// mpv_render_context_free() can block waiting for mpv's render/VO thread,
|
||||
// and mpv_terminate_destroy() can block on demuxer/network I/O.
|
||||
// Running these off the main thread prevents stalling the GLib main loop.
|
||||
// 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;
|
||||
|
||||
std::thread([gl, handle, egl_display, egl_context]() {
|
||||
if (gl) {
|
||||
// mpv render context must be freed with its EGL context current
|
||||
if (egl_context != EGL_NO_CONTEXT) {
|
||||
eglMakeCurrent(egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, egl_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);
|
||||
}
|
||||
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();
|
||||
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();
|
||||
}
|
||||
|
||||
observed_properties_.Clear();
|
||||
}
|
||||
@@ -325,7 +379,7 @@ void MpvPlayer::SetProperty(const std::string& name, const std::string& value) {
|
||||
|
||||
void MpvPlayer::SetPropertyAsync(const std::string& name, const std::string& value, StatusCallback callback) {
|
||||
if (disposed_ || !mpv_) {
|
||||
if (callback) callback(0);
|
||||
if (callback) callback(MPV_ERROR_UNINITIALIZED);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -391,25 +445,22 @@ void MpvPlayer::SetLogLevel(const std::string& level) {
|
||||
}
|
||||
|
||||
void MpvPlayer::OnMpvWakeup(void* ctx) {
|
||||
auto* player = static_cast<MpvPlayer*>(ctx);
|
||||
auto* context = static_cast<CallbackContext*>(ctx);
|
||||
auto lease = context->Acquire();
|
||||
if (!lease) return;
|
||||
|
||||
if (player->disposed_) return;
|
||||
|
||||
g_idle_add_full(
|
||||
G_PRIORITY_HIGH_IDLE,
|
||||
[](gpointer data) -> gboolean {
|
||||
auto* player = static_cast<MpvPlayer*>(data);
|
||||
|
||||
if (!player->disposed_ && player->mpv_) {
|
||||
player->ProcessEvents();
|
||||
}
|
||||
return G_SOURCE_REMOVE;
|
||||
},
|
||||
player, nullptr);
|
||||
MpvPlayer* player = lease.player();
|
||||
if (!player->disposed_) {
|
||||
player->ScheduleWakeupSource();
|
||||
}
|
||||
}
|
||||
|
||||
void MpvPlayer::OnMpvRenderUpdate(void* ctx) {
|
||||
auto* player = static_cast<MpvPlayer*>(ctx);
|
||||
auto* context = static_cast<CallbackContext*>(ctx);
|
||||
auto lease = context->Acquire();
|
||||
if (!lease) return;
|
||||
|
||||
MpvPlayer* player = lease.player();
|
||||
if (player->disposed_) return;
|
||||
|
||||
bool expected = false;
|
||||
@@ -417,23 +468,127 @@ void MpvPlayer::OnMpvRenderUpdate(void* ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Schedule redraw on main thread. Calling Flutter's
|
||||
// fl_texture_registrar_mark_texture_frame_available directly from mpv's
|
||||
// render/VO thread can deadlock during disposal on Wayland: the main thread
|
||||
// blocks in mpv_render_context_free() waiting for the VO thread, while the
|
||||
// VO thread blocks in the Flutter registrar waiting for the main thread.
|
||||
g_idle_add(
|
||||
[](gpointer data) -> gboolean {
|
||||
auto* player = static_cast<MpvPlayer*>(data);
|
||||
if (player->disposed_) return G_SOURCE_REMOVE;
|
||||
// Flutter texture notification must run on the player's owning GLib
|
||||
// context, never on mpv's render/VO thread.
|
||||
player->ScheduleRedrawSource();
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(player->callback_mutex_);
|
||||
if (player->redraw_callback_) {
|
||||
player->redraw_callback_();
|
||||
}
|
||||
return G_SOURCE_REMOVE;
|
||||
},
|
||||
player);
|
||||
void MpvPlayer::DestroySourceCallbackData(gpointer data) { delete static_cast<SourceCallbackData*>(data); }
|
||||
|
||||
void MpvPlayer::ScheduleWakeupSource() {
|
||||
std::lock_guard<std::mutex> lock(source_mutex_);
|
||||
if (disposed_ || wakeup_source_id_ != 0) return;
|
||||
|
||||
GSource* source = g_idle_source_new();
|
||||
g_source_set_priority(source, G_PRIORITY_HIGH_IDLE);
|
||||
auto* data = new SourceCallbackData(callback_context_);
|
||||
g_source_set_callback(source, DispatchWakeupSource, data, DestroySourceCallbackData);
|
||||
data->source_id = g_source_attach(source, callback_context_->main_context());
|
||||
wakeup_source_id_ = data->source_id;
|
||||
g_source_unref(source);
|
||||
}
|
||||
|
||||
void MpvPlayer::ScheduleRedrawSource() {
|
||||
std::lock_guard<std::mutex> lock(source_mutex_);
|
||||
if (disposed_ || redraw_source_id_ != 0) return;
|
||||
|
||||
GSource* source = g_idle_source_new();
|
||||
auto* data = new SourceCallbackData(callback_context_);
|
||||
g_source_set_callback(source, DispatchRedrawSource, data, DestroySourceCallbackData);
|
||||
data->source_id = g_source_attach(source, callback_context_->main_context());
|
||||
redraw_source_id_ = data->source_id;
|
||||
g_source_unref(source);
|
||||
|
||||
if (redraw_source_id_ == 0) {
|
||||
needs_redraw_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
void MpvPlayer::ScheduleRecoverySource() {
|
||||
std::lock_guard<std::mutex> lock(source_mutex_);
|
||||
if (disposed_ || recovery_source_id_ != 0) return;
|
||||
|
||||
GSource* source = g_timeout_source_new(100);
|
||||
auto* data = new SourceCallbackData(callback_context_);
|
||||
g_source_set_callback(source, DispatchRecoverySource, data, DestroySourceCallbackData);
|
||||
data->source_id = g_source_attach(source, callback_context_->main_context());
|
||||
recovery_source_id_ = data->source_id;
|
||||
g_source_unref(source);
|
||||
}
|
||||
|
||||
gboolean MpvPlayer::DispatchWakeupSource(gpointer data) {
|
||||
auto* source_data = static_cast<SourceCallbackData*>(data);
|
||||
auto lease = source_data->context->Acquire();
|
||||
if (!lease) return G_SOURCE_REMOVE;
|
||||
|
||||
MpvPlayer* player = lease.player();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(player->source_mutex_);
|
||||
if (player->wakeup_source_id_ == source_data->source_id) {
|
||||
player->wakeup_source_id_ = 0;
|
||||
}
|
||||
}
|
||||
if (!player->disposed_ && player->mpv_) {
|
||||
player->ProcessEvents();
|
||||
}
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
gboolean MpvPlayer::DispatchRedrawSource(gpointer data) {
|
||||
auto* source_data = static_cast<SourceCallbackData*>(data);
|
||||
auto lease = source_data->context->Acquire();
|
||||
if (!lease) return G_SOURCE_REMOVE;
|
||||
|
||||
MpvPlayer* player = lease.player();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(player->source_mutex_);
|
||||
if (player->redraw_source_id_ == source_data->source_id) {
|
||||
player->redraw_source_id_ = 0;
|
||||
}
|
||||
}
|
||||
if (player->disposed_) return G_SOURCE_REMOVE;
|
||||
|
||||
RedrawCallback callback;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(player->callback_mutex_);
|
||||
callback = player->redraw_callback_;
|
||||
}
|
||||
if (callback) callback();
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
gboolean MpvPlayer::DispatchRecoverySource(gpointer data) {
|
||||
auto* source_data = static_cast<SourceCallbackData*>(data);
|
||||
auto lease = source_data->context->Acquire();
|
||||
if (!lease) return G_SOURCE_REMOVE;
|
||||
|
||||
MpvPlayer* player = lease.player();
|
||||
if (player->disposed_) return G_SOURCE_REMOVE;
|
||||
|
||||
player->MaybeRunAudioRecovery();
|
||||
if (player->audio_recovery_.HasPendingWork()) {
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(player->source_mutex_);
|
||||
if (player->recovery_source_id_ == source_data->source_id) {
|
||||
player->recovery_source_id_ = 0;
|
||||
}
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
void MpvPlayer::RemoveTrackedSources() {
|
||||
std::lock_guard<std::mutex> lock(source_mutex_);
|
||||
GMainContext* context = callback_context_->main_context();
|
||||
auto remove = [context](guint& source_id) {
|
||||
if (source_id == 0) return;
|
||||
GSource* source = g_main_context_find_source_by_id(context, source_id);
|
||||
if (source) g_source_destroy(source);
|
||||
source_id = 0;
|
||||
};
|
||||
remove(wakeup_source_id_);
|
||||
remove(redraw_source_id_);
|
||||
remove(recovery_source_id_);
|
||||
}
|
||||
|
||||
bool MpvPlayer::ProcessEvents() {
|
||||
@@ -486,23 +641,8 @@ void MpvPlayer::MaybeRunAudioRecovery() {
|
||||
}
|
||||
|
||||
void MpvPlayer::EnsureAudioRecoveryTimer() {
|
||||
if (recovery_source_id_ != 0 || !audio_recovery_.HasPendingWork()) return;
|
||||
recovery_source_id_ = g_timeout_add(
|
||||
100,
|
||||
[](gpointer data) -> gboolean {
|
||||
auto* player = static_cast<MpvPlayer*>(data);
|
||||
if (player->disposed_) {
|
||||
player->recovery_source_id_ = 0;
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
player->MaybeRunAudioRecovery();
|
||||
if (!player->audio_recovery_.HasPendingWork()) {
|
||||
player->recovery_source_id_ = 0;
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
return G_SOURCE_CONTINUE;
|
||||
},
|
||||
this);
|
||||
if (!audio_recovery_.HasPendingWork()) return;
|
||||
ScheduleRecoverySource();
|
||||
}
|
||||
|
||||
void MpvPlayer::HandleMpvEvent(mpv_event* event) {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <mpv/render_gl.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
@@ -120,12 +121,66 @@ class MpvPlayer {
|
||||
void SetLogLevel(const std::string& level);
|
||||
|
||||
private:
|
||||
class CallbackContext {
|
||||
public:
|
||||
class Lease {
|
||||
public:
|
||||
Lease() = default;
|
||||
Lease(const Lease&) = delete;
|
||||
Lease& operator=(const Lease&) = delete;
|
||||
Lease(Lease&& other) noexcept;
|
||||
Lease& operator=(Lease&& other) noexcept;
|
||||
~Lease();
|
||||
|
||||
explicit operator bool() const { return player_ != nullptr; }
|
||||
MpvPlayer* player() const { return player_; }
|
||||
|
||||
private:
|
||||
friend class CallbackContext;
|
||||
Lease(CallbackContext* context, MpvPlayer* player);
|
||||
void Release();
|
||||
|
||||
CallbackContext* context_ = nullptr;
|
||||
MpvPlayer* player_ = nullptr;
|
||||
};
|
||||
|
||||
explicit CallbackContext(MpvPlayer* player);
|
||||
~CallbackContext();
|
||||
|
||||
Lease Acquire();
|
||||
void DetachAndWait();
|
||||
GMainContext* main_context() const { return main_context_; }
|
||||
|
||||
private:
|
||||
void ReleaseLease();
|
||||
|
||||
std::mutex mutex_;
|
||||
std::condition_variable quiescent_;
|
||||
MpvPlayer* player_;
|
||||
size_t in_flight_ = 0;
|
||||
GMainContext* main_context_;
|
||||
};
|
||||
|
||||
struct SourceCallbackData;
|
||||
|
||||
friend class MpvPlayerLifecycleTestPeer;
|
||||
|
||||
/// MPV event wakeup callback (called from mpv thread).
|
||||
static void OnMpvWakeup(void* ctx);
|
||||
|
||||
/// MPV render update callback (called when frame is ready).
|
||||
static void OnMpvRenderUpdate(void* ctx);
|
||||
|
||||
static gboolean DispatchWakeupSource(gpointer data);
|
||||
static gboolean DispatchRedrawSource(gpointer data);
|
||||
static gboolean DispatchRecoverySource(gpointer data);
|
||||
static void DestroySourceCallbackData(gpointer data);
|
||||
|
||||
void ScheduleWakeupSource();
|
||||
void ScheduleRedrawSource();
|
||||
void ScheduleRecoverySource();
|
||||
void RemoveTrackedSources();
|
||||
|
||||
/// Processes pending mpv events.
|
||||
bool ProcessEvents();
|
||||
|
||||
@@ -164,8 +219,12 @@ class MpvPlayer {
|
||||
plezy::mpv_common::PropertyObservationRegistry observed_properties_;
|
||||
bool hdr_enabled_ = true;
|
||||
|
||||
// GLib sources for event delivery and scheduled audio recovery.
|
||||
guint event_source_id_ = 0;
|
||||
// All player-carrying sources are attached to CallbackContext::main_context()
|
||||
// and protected by source_mutex_.
|
||||
std::shared_ptr<CallbackContext> callback_context_;
|
||||
std::mutex source_mutex_;
|
||||
guint wakeup_source_id_ = 0;
|
||||
guint redraw_source_id_ = 0;
|
||||
guint recovery_source_id_ = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
#include "mpv_player.h"
|
||||
|
||||
namespace mpv {
|
||||
|
||||
class MpvPlayerLifecycleTestPeer {
|
||||
public:
|
||||
static std::shared_ptr<MpvPlayer::CallbackContext> RetainContext(MpvPlayer& player) {
|
||||
return player.callback_context_;
|
||||
}
|
||||
|
||||
static void Wakeup(const std::shared_ptr<MpvPlayer::CallbackContext>& context) {
|
||||
MpvPlayer::OnMpvWakeup(context.get());
|
||||
}
|
||||
|
||||
static void RenderUpdate(const std::shared_ptr<MpvPlayer::CallbackContext>& context) {
|
||||
MpvPlayer::OnMpvRenderUpdate(context.get());
|
||||
}
|
||||
|
||||
static void ScheduleRecovery(MpvPlayer& player) { player.ScheduleRecoverySource(); }
|
||||
|
||||
static void RegisterPendingPropertyWrite(MpvPlayer& player, MpvPlayer::StatusCallback callback) {
|
||||
player.pending_requests_.RegisterStatus(std::move(callback));
|
||||
}
|
||||
|
||||
static int PendingSourceCount(MpvPlayer& player) {
|
||||
std::lock_guard<std::mutex> lock(player.source_mutex_);
|
||||
return (player.wakeup_source_id_ != 0 ? 1 : 0) + (player.redraw_source_id_ != 0 ? 1 : 0) +
|
||||
(player.recovery_source_id_ != 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
static void HoldLease(
|
||||
const std::shared_ptr<MpvPlayer::CallbackContext>& context, std::mutex& mutex, std::condition_variable& condition,
|
||||
bool& entered, bool& release) {
|
||||
auto lease = context->Acquire();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
entered = static_cast<bool>(lease);
|
||||
}
|
||||
condition.notify_all();
|
||||
|
||||
std::unique_lock<std::mutex> lock(mutex);
|
||||
condition.wait(lock, [&release]() { return release; });
|
||||
}
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
void Check(bool condition, const char* message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
void Drain(GMainContext* context) {
|
||||
while (g_main_context_iteration(context, FALSE)) {
|
||||
}
|
||||
}
|
||||
|
||||
void TestUnavailablePropertyWriteFails() {
|
||||
MpvPlayer player;
|
||||
int callback_count = 0;
|
||||
int status = MPV_ERROR_SUCCESS;
|
||||
|
||||
player.SetPropertyAsync("pause", "yes", [&](int error) {
|
||||
++callback_count;
|
||||
status = error;
|
||||
});
|
||||
|
||||
Check(callback_count == 1, "a property write without an mpv handle must complete exactly once");
|
||||
Check(status == MPV_ERROR_UNINITIALIZED, "a property write without an mpv handle must fail as uninitialized");
|
||||
}
|
||||
|
||||
void TestPendingPropertyWriteFailsOnDispose() {
|
||||
MpvPlayer player;
|
||||
int callback_count = 0;
|
||||
int status = MPV_ERROR_SUCCESS;
|
||||
MpvPlayerLifecycleTestPeer::RegisterPendingPropertyWrite(player, [&](int error) {
|
||||
++callback_count;
|
||||
status = error;
|
||||
});
|
||||
|
||||
player.Dispose();
|
||||
Check(callback_count == 1, "dispose must complete a pending property write exactly once");
|
||||
Check(status < 0, "dispose must fail a pending property write");
|
||||
|
||||
player.Dispose();
|
||||
Check(callback_count == 1, "repeated dispose must not complete a property write twice");
|
||||
}
|
||||
|
||||
void TestQueuedSourcesAreRetired(GMainContext* context) {
|
||||
int redraws = 0;
|
||||
auto player = std::make_unique<MpvPlayer>();
|
||||
auto callback_context = MpvPlayerLifecycleTestPeer::RetainContext(*player);
|
||||
player->SetRedrawCallback([&redraws]() { ++redraws; });
|
||||
|
||||
MpvPlayerLifecycleTestPeer::Wakeup(callback_context);
|
||||
MpvPlayerLifecycleTestPeer::RenderUpdate(callback_context);
|
||||
MpvPlayerLifecycleTestPeer::ScheduleRecovery(*player);
|
||||
Check(MpvPlayerLifecycleTestPeer::PendingSourceCount(*player) == 3, "all player sources must be tracked");
|
||||
|
||||
player->Dispose();
|
||||
Check(MpvPlayerLifecycleTestPeer::PendingSourceCount(*player) == 0, "dispose must retire every tracked source");
|
||||
player.reset();
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
void TestNativeLeaseBlocksDispose() {
|
||||
auto player = std::make_unique<MpvPlayer>();
|
||||
auto callback_context = MpvPlayerLifecycleTestPeer::RetainContext(*player);
|
||||
std::mutex mutex;
|
||||
std::condition_variable condition;
|
||||
bool entered = false;
|
||||
bool release = false;
|
||||
|
||||
std::thread holder(
|
||||
[&]() { MpvPlayerLifecycleTestPeer::HoldLease(callback_context, mutex, condition, entered, release); });
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mutex);
|
||||
condition.wait(lock, [&entered]() { return entered; });
|
||||
}
|
||||
|
||||
std::atomic<bool> disposed{false};
|
||||
std::thread disposer([&]() {
|
||||
player->Dispose();
|
||||
disposed = true;
|
||||
});
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(25));
|
||||
Check(!disposed.load(), "dispose returned while a native callback lease was active");
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
release = true;
|
||||
}
|
||||
condition.notify_all();
|
||||
holder.join();
|
||||
disposer.join();
|
||||
Check(disposed.load(), "dispose did not finish after the native callback lease was released");
|
||||
|
||||
player.reset();
|
||||
MpvPlayerLifecycleTestPeer::Wakeup(callback_context);
|
||||
MpvPlayerLifecycleTestPeer::RenderUpdate(callback_context);
|
||||
}
|
||||
|
||||
void TestWakeupAndRedrawCoalesce(GMainContext* context) {
|
||||
int redraws = 0;
|
||||
MpvPlayer player;
|
||||
auto callback_context = MpvPlayerLifecycleTestPeer::RetainContext(player);
|
||||
player.SetRedrawCallback([&redraws]() { ++redraws; });
|
||||
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
MpvPlayerLifecycleTestPeer::Wakeup(callback_context);
|
||||
MpvPlayerLifecycleTestPeer::RenderUpdate(callback_context);
|
||||
}
|
||||
Check(MpvPlayerLifecycleTestPeer::PendingSourceCount(player) == 2, "wakeup and redraw sources must coalesce");
|
||||
Drain(context);
|
||||
Check(redraws == 1, "coalesced redraw was not delivered exactly once");
|
||||
Check(MpvPlayerLifecycleTestPeer::PendingSourceCount(player) == 0, "dispatched source IDs must be cleared");
|
||||
|
||||
player.ClearRedrawFlag();
|
||||
MpvPlayerLifecycleTestPeer::RenderUpdate(callback_context);
|
||||
Drain(context);
|
||||
Check(redraws == 2, "a redraw after dispatch must still be delivered");
|
||||
}
|
||||
|
||||
void TestRapidReplacementCannotReceiveOldCallbacks(GMainContext* context) {
|
||||
for (int iteration = 0; iteration < 100; ++iteration) {
|
||||
int old_redraws = 0;
|
||||
int replacement_redraws = 0;
|
||||
|
||||
auto old_player = std::make_unique<MpvPlayer>();
|
||||
auto old_context = MpvPlayerLifecycleTestPeer::RetainContext(*old_player);
|
||||
old_player->SetRedrawCallback([&old_redraws]() { ++old_redraws; });
|
||||
MpvPlayerLifecycleTestPeer::Wakeup(old_context);
|
||||
MpvPlayerLifecycleTestPeer::RenderUpdate(old_context);
|
||||
old_player->Dispose();
|
||||
old_player.reset();
|
||||
|
||||
auto replacement = std::make_unique<MpvPlayer>();
|
||||
auto replacement_context = MpvPlayerLifecycleTestPeer::RetainContext(*replacement);
|
||||
replacement->SetRedrawCallback([&replacement_redraws]() { ++replacement_redraws; });
|
||||
|
||||
// Simulate both an entered-old callback resuming and fresh replacement work.
|
||||
MpvPlayerLifecycleTestPeer::Wakeup(old_context);
|
||||
MpvPlayerLifecycleTestPeer::RenderUpdate(old_context);
|
||||
MpvPlayerLifecycleTestPeer::Wakeup(replacement_context);
|
||||
MpvPlayerLifecycleTestPeer::RenderUpdate(replacement_context);
|
||||
Drain(context);
|
||||
|
||||
Check(old_redraws == 0, "an old redraw callback ran after replacement");
|
||||
Check(replacement_redraws == 1, "old callback state suppressed or duplicated a replacement redraw");
|
||||
replacement->Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mpv
|
||||
|
||||
int main() {
|
||||
GMainContext* context = g_main_context_new();
|
||||
g_main_context_push_thread_default(context);
|
||||
|
||||
try {
|
||||
mpv::TestUnavailablePropertyWriteFails();
|
||||
mpv::TestPendingPropertyWriteFailsOnDispose();
|
||||
mpv::TestQueuedSourcesAreRetired(context);
|
||||
mpv::TestNativeLeaseBlocksDispose();
|
||||
mpv::TestWakeupAndRedrawCoalesce(context);
|
||||
mpv::TestRapidReplacementCannotReceiveOldCallbacks(context);
|
||||
} catch (const std::exception& error) {
|
||||
g_main_context_pop_thread_default(context);
|
||||
g_main_context_unref(context);
|
||||
std::cerr << "mpv_player_lifecycle_test: " << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
|
||||
g_main_context_pop_thread_default(context);
|
||||
g_main_context_unref(context);
|
||||
std::cout << "mpv_player_lifecycle_test: PASS\n";
|
||||
return 0;
|
||||
}
|
||||
@@ -186,8 +186,7 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel, FlMethodCall
|
||||
}
|
||||
if (self->player) {
|
||||
self->player->Dispose();
|
||||
// Don't reset player here — stray g_idle callbacks still reference it.
|
||||
// It will be replaced on next initialize() call.
|
||||
self->player.reset();
|
||||
}
|
||||
self->initialized = FALSE;
|
||||
self->visible = FALSE;
|
||||
@@ -225,7 +224,8 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel, FlMethodCall
|
||||
}
|
||||
} else if (strcmp(method, "setProperty") == 0) {
|
||||
if (!self->player || !self->initialized) {
|
||||
response = FL_METHOD_RESPONSE(fl_method_error_response_new("NOT_INITIALIZED", "Player not initialized", nullptr));
|
||||
response = FL_METHOD_RESPONSE(fl_method_error_response_new(
|
||||
plezy::mpv_common::kSetPropertyNotInitializedCode, "Player not initialized", nullptr));
|
||||
} else {
|
||||
FlValue* name_value = fl_value_lookup_string(args, "name");
|
||||
FlValue* value_value = fl_value_lookup_string(args, "value");
|
||||
@@ -238,7 +238,14 @@ static void mpv_plugin_handle_method_call(FlMethodChannel* channel, FlMethodCall
|
||||
g_object_ref(method_call);
|
||||
self->player->SetPropertyAsync(
|
||||
fl_value_get_string(name_value), fl_value_get_string(value_value), [method_call](int error) {
|
||||
g_autoptr(FlMethodResponse) async_response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||
g_autoptr(FlMethodResponse) async_response = nullptr;
|
||||
if (plezy::mpv_common::SetPropertyStatusSucceeded(error)) {
|
||||
async_response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
|
||||
} else {
|
||||
const std::string description = plezy::mpv_common::SetPropertyErrorDescription(error);
|
||||
async_response = FL_METHOD_RESPONSE(fl_method_error_response_new(
|
||||
plezy::mpv_common::kSetPropertyFailedCode, description.c_str(), nullptr));
|
||||
}
|
||||
fl_method_call_respond(method_call, async_response, nullptr);
|
||||
g_object_unref(method_call);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user