fix(runtime): harden application service boundaries

This commit is contained in:
edde746
2026-07-24 03:46:46 +02:00
parent 658da37b48
commit e0bf66eea8
309 changed files with 32574 additions and 4369 deletions
+50
View File
@@ -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
View File
@@ -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) {
+61 -2
View File
@@ -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;
}
+11 -4
View File
@@ -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);
});