#include "mpv_player.h" #include #include #include #include #ifdef GDK_WINDOWING_X11 #include #endif #ifdef GDK_WINDOWING_WAYLAND #include #endif #include #include #include #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; return reinterpret_cast(eglGetProcAddress(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 lock(mutex_); batches_.push_back(std::move(batch)); ++generation_; } condition_.notify_one(); } void Retry() { { std::lock_guard 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 lock(mutex_); for (;;) { condition_.wait(lock, [this]() { return !batches_.empty(); }); const uint64_t observed_generation = generation_; std::vector 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 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 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& 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) {} 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 lock(mutex_); if (!player_) return Lease(); ++in_flight_; return Lease(this, player_); } void MpvPlayer::CallbackContext::WaitUntilDetached() { std::unique_lock lock(mutex_); quiescent_.wait(lock, [this]() { return player_ == nullptr; }); } void MpvPlayer::CallbackContext::DetachAndWait() { std::unique_lock lock(mutex_); player_ = nullptr; quiescent_.notify_all(); quiescent_.wait(lock, [this]() { return in_flight_ == 0; }); } void MpvPlayer::CallbackContext::ReleaseLease() { std::lock_guard lock(mutex_); --in_flight_; if (in_flight_ == 0) quiescent_.notify_all(); } struct MpvPlayer::SourceCallbackData { explicit SourceCallbackData(std::shared_ptr callback_context) : context(std::move(callback_context)) {} std::shared_ptr context; guint source_id = 0; }; MpvPlayer::MpvPlayer(bool audio_only) : audio_only_(audio_only), callback_context_(std::make_shared(this)) {} MpvPlayer::~MpvPlayer() { Dispose(); } bool MpvPlayer::HasRenderContext() const { std::lock_guard lock(native_mutex_); return mpv_gl_ != nullptr; } EGLDisplay MpvPlayer::GetEglDisplay() const { std::lock_guard lock(native_mutex_); return egl_display_; } EGLContext MpvPlayer::GetEglContext() const { std::lock_guard lock(native_mutex_); return egl_context_; } bool MpvPlayer::IsInitialized() const { std::lock_guard lock(native_mutex_); return mpv_ != nullptr && (audio_only_ || mpv_gl_ != nullptr); } bool MpvPlayer::HasMpvHandle() const { std::lock_guard lock(native_mutex_); return mpv_ != nullptr; } bool MpvPlayer::Initialize() { std::lock_guard lock(native_mutex_); if (disposed_) { g_warning("MPV: initialization requested after disposal"); return false; } if (mpv_) { return true; // Already initialized. } if (!EnsureProcessNumericLocale()) { g_warning("MPV: Failed to establish the process-wide C numeric locale"); return false; } // Create mpv instance. mpv_ = mpv_create(); if (!mpv_) { g_warning("MPV: mpv_create() failed"); return false; } if (audio_only_) { // Music core: no VO, no video decode. vid=no keeps embedded cover art // from ever becoming a video track, and force-window/audio-display make // sure mpv never opens a video output for it either. mpv_set_option_string(mpv_, "vid", "no"); mpv_set_option_string(mpv_, "force-window", "no"); mpv_set_option_string(mpv_, "audio-display", "no"); mpv_set_option_string(mpv_, "gapless-audio", "weak"); } else { // Configure mpv for embedded playback. mpv_set_option_string(mpv_, "vo", "libmpv"); mpv_set_option_string(mpv_, "hwdec", "auto"); } mpv_set_option_string(mpv_, "keep-open", "yes"); mpv_set_option_string(mpv_, "audio-fallback-to-null", "yes"); if (!audio_only_) { // HDR tone mapping mpv_set_option_string(mpv_, "tone-mapping", "auto"); mpv_set_option_string(mpv_, "target-colorspace-hint", plezy::mpv_common::TargetColorspaceHint(hdr_enabled_)); mpv_set_option_string(mpv_, "hdr-compute-peak", "auto"); } mpv_set_option_string(mpv_, "idle", "yes"); mpv_set_option_string(mpv_, "input-default-bindings", "no"); mpv_set_option_string(mpv_, "input-vo-keyboard", "no"); mpv_set_option_string(mpv_, "osc", "no"); mpv_set_option_string(mpv_, "terminal", "no"); // Default to warn-level logging mpv_request_log_messages(mpv_, "warn"); // Initialize mpv. int err = mpv_initialize(mpv_); if (err < 0) { g_warning("MPV: mpv_initialize() failed: %s", mpv_error_string(err)); mpv_destroy(mpv_); mpv_ = nullptr; return false; } // Set up event wakeup callback. 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); g_message("MPV: Initialization successful (%s)", audio_only_ ? "audio-only" : "render context deferred"); return true; } void MpvPlayer::RetryPendingNativeTeardown() { NativeRenderTeardownQueue::Instance().Retry(); } bool MpvPlayer::InitRenderContext() { RetryPendingNativeTeardown(); std::lock_guard lock(native_mutex_); if (audio_only_ || disposed_) { g_warning("MPV: Render context requested for an unavailable player"); return false; } if (mpv_gl_) return true; if (!mpv_) { g_warning("MPV: Cannot create render context - mpv not initialized"); return false; } 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; } 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; } EGLint config_id = 0; if (!eglQueryContext(flutter_display, flutter_context, EGL_CONFIG_ID, &config_id)) { g_warning("MPV: Failed to query Flutter EGL config: 0x%x", eglGetError()); return false; } EGLConfig config = nullptr; EGLint num_configs = 0; const EGLint config_attribs[] = {EGL_CONFIG_ID, config_id, EGL_NONE}; if (!eglChooseConfig(flutter_display, config_attribs, &config, 1, &num_configs) || num_configs != 1) { g_warning("MPV: Failed to select Flutter EGL config: 0x%x", eglGetError()); return false; } if (!eglBindAPI(EGL_OPENGL_ES_API)) { g_warning("MPV: Failed to bind OpenGL ES API: 0x%x", eglGetError()); return false; } const EGLint context_attribs[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE}; EGLContext candidate_context = eglCreateContext(flutter_display, config, EGL_NO_CONTEXT, context_attribs); if (candidate_context == EGL_NO_CONTEXT) { g_warning("MPV: Failed to create isolated EGL context: 0x%x", eglGetError()); if (previous_api != EGL_NONE && !eglBindAPI(previous_api)) { g_warning("MPV: Failed to restore EGL client API: 0x%x", eglGetError()); } return false; } auto destroy_candidate_context = [&]() { const EGLenum api_before_cleanup = eglQueryAPI(); if (eglGetCurrentContext() == candidate_context) { if (!eglBindAPI(EGL_OPENGL_ES_API) || !eglMakeCurrent(flutter_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)) { g_warning("MPV: Failed to release rejected EGL context: 0x%x", eglGetError()); return; } } if (!eglDestroyContext(flutter_display, candidate_context)) { g_warning("MPV: Failed to destroy rejected EGL context: 0x%x", eglGetError()); } if (api_before_cleanup != EGL_NONE && !eglBindAPI(api_before_cleanup)) { g_warning("MPV: Failed to restore EGL API after context cleanup: 0x%x", eglGetError()); } }; if (!eglMakeCurrent(flutter_display, EGL_NO_SURFACE, EGL_NO_SURFACE, candidate_context)) { g_warning("MPV: Failed to activate isolated EGL context: 0x%x", eglGetError()); destroy_candidate_context(); if (previous_api != EGL_NONE && !eglBindAPI(previous_api)) { g_warning("MPV: Failed to restore EGL client API: 0x%x", eglGetError()); } return false; } mpv_opengl_init_params gl_init_params{}; gl_init_params.get_proc_address = get_opengl_proc_address; gl_init_params.get_proc_address_ctx = nullptr; mpv_render_param params[] = { {MPV_RENDER_PARAM_API_TYPE, const_cast(MPV_RENDER_API_TYPE_OPENGL)}, {MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, &gl_init_params}, {MPV_RENDER_PARAM_INVALID, nullptr}, {MPV_RENDER_PARAM_INVALID, nullptr}, }; GdkDisplay* gdk_display = gdk_display_get_default(); #ifdef GDK_WINDOWING_WAYLAND if (GDK_IS_WAYLAND_DISPLAY(gdk_display)) { params[2].type = MPV_RENDER_PARAM_WL_DISPLAY; params[2].data = gdk_wayland_display_get_wl_display(gdk_display); } #endif #ifdef GDK_WINDOWING_X11 if (GDK_IS_X11_DISPLAY(gdk_display)) { params[2].type = MPV_RENDER_PARAM_X11_DISPLAY; params[2].data = gdk_x11_display_get_xdisplay(gdk_display); } #endif mpv_render_context* candidate_gl = nullptr; const int error = mpv_render_context_create(&candidate_gl, mpv_, params); const bool restored = restore_flutter(); if (error < 0 || candidate_gl == nullptr || !restored) { if (error < 0) { g_warning("MPV: mpv_render_context_create() failed: %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; } 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; } void MpvPlayer::Dispose() { if (disposed_.exchange(true)) { return; } // 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. { std::lock_guard 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(); { std::lock_guard lock(callback_mutex_); redraw_callback_ = nullptr; event_callback_ = nullptr; } auto cancelled = pending_requests_.CancelAll(); for (auto& callback : cancelled.status) { callback(-1); } for (auto& callback : cancelled.properties) { callback(-1, ""); } RemoveTrackedSources(); // 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 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 lock(native_mutex_); if (disposed_ || !mpv_gl_) return; mpv_opengl_fbo mpv_fbo{}; mpv_fbo.fbo = fbo; mpv_fbo.w = width; mpv_fbo.h = height; 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); } void MpvPlayer::Command(const std::vector& args) { CommandAsync(args, nullptr); } void MpvPlayer::CommandAsync(const std::vector& args, CommandCallback callback) { if (disposed_ || !mpv_) { if (callback) callback(MPV_ERROR_UNINITIALIZED); return; } plezy::mpv_common::SubmitCommandAsync(mpv_, pending_requests_, args, std::move(callback)); } void MpvPlayer::SetProperty(const std::string& name, const std::string& value) { SetPropertyAsync(name, value, nullptr); } void MpvPlayer::SetPropertyAsync(const std::string& name, const std::string& value, StatusCallback callback) { if (disposed_ || !mpv_) { if (callback) callback(MPV_ERROR_UNINITIALIZED); return; } if (name == "hdr-enabled") { SetHDREnabled(plezy::mpv_common::ParseEnabledFlag(value), std::move(callback)); 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); }; } plezy::mpv_common::SubmitSetPropertyAsync(mpv_, pending_requests_, name, value, std::move(callback)); } void MpvPlayer::GetPropertyAsync(const std::string& name, GetPropertyCallback callback) { if (disposed_ || !mpv_) { if (callback) callback(MPV_ERROR_UNINITIALIZED, ""); return; } plezy::mpv_common::SubmitGetPropertyAsync(mpv_, pending_requests_, name, std::move(callback)); } void MpvPlayer::ObserveProperty(const std::string& name, const std::string& format, int id) { if (disposed_ || !mpv_) return; const auto request = observed_properties_.Register(name, format, id); if (!request.added) return; mpv_observe_property(mpv_, request.userdata, name.c_str(), request.format); } void MpvPlayer::ReportMouseMove(int x, int y) { if (disposed_ || !mpv_) return; std::string x_str = std::to_string(x); std::string y_str = std::to_string(y); const char* args[] = {"mouse", x_str.c_str(), y_str.c_str(), nullptr}; mpv_command_async(mpv_, 0, args); } void MpvPlayer::SetEventCallback(EventCallback callback) { std::lock_guard lock(callback_mutex_); event_callback_ = std::move(callback); } void MpvPlayer::SetRedrawCallback(RedrawCallback callback) { std::lock_guard lock(callback_mutex_); redraw_callback_ = std::move(callback); } void MpvPlayer::SetLogLevel(const std::string& level) { if (disposed_ || !mpv_) return; mpv_request_log_messages(mpv_, level.c_str()); } void MpvPlayer::OnMpvWakeup(void* ctx) { auto* context = static_cast(ctx); auto lease = context->Acquire(); if (!lease) return; MpvPlayer* player = lease.player(); if (!player->disposed_) { player->ScheduleWakeupSource(); } } void MpvPlayer::OnMpvRenderUpdate(void* ctx) { auto* context = static_cast(ctx); auto lease = context->Acquire(); if (!lease) return; MpvPlayer* player = lease.player(); if (player->disposed_) return; bool expected = false; if (!player->needs_redraw_.compare_exchange_strong(expected, true)) { return; } // Flutter texture notification must run on the player's owning GLib // context, never on mpv's render/VO thread. player->ScheduleRedrawSource(); } void MpvPlayer::DestroySourceCallbackData(gpointer data) { delete static_cast(data); } void MpvPlayer::ScheduleWakeupSource() { std::lock_guard 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 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 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(data); auto lease = source_data->context->Acquire(); if (!lease) return G_SOURCE_REMOVE; MpvPlayer* player = lease.player(); { std::lock_guard 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(data); auto lease = source_data->context->Acquire(); if (!lease) return G_SOURCE_REMOVE; MpvPlayer* player = lease.player(); { std::lock_guard 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 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(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 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 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() { if (disposed_ || !mpv_) return false; while (true) { mpv_event* event = mpv_wait_event(mpv_, 0); if (event->event_id == MPV_EVENT_NONE) { break; } if (event->event_id == MPV_EVENT_SHUTDOWN) { return false; } HandleMpvEvent(event); } return true; } void MpvPlayer::LogRecovery(const std::string& text) { g_warning("MPV audio-recovery: %s", text.c_str()); FlValue* data = fl_value_new_map(); fl_value_set_string_take(data, "prefix", fl_value_new_string("audio-recovery")); fl_value_set_string_take(data, "level", fl_value_new_string("warn")); fl_value_set_string_take(data, "text", fl_value_new_string(text.c_str())); SendEvent("log-message", data); fl_value_unref(data); } 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; 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) + ")"); }); } void MpvPlayer::MaybeRunAudioRecovery() { const auto action = audio_recovery_.NextReload(plezy::mpv_common::AudioRecoveryState::Clock::now()); if (action.reason == plezy::mpv_common::AudioReloadReason::kNone) { return; } const char* reason = action.reason == plezy::mpv_common::AudioReloadReason::kResume ? "resume" : "null-fallback"; TryAudioReload(reason, action.attempt, action.request_generation); if (action.exhausted) { LogRecovery("audio recovery budget exhausted; waiting for device list change"); } } void MpvPlayer::EnsureAudioRecoveryTimer() { if (!audio_recovery_.HasPendingWork()) return; ScheduleRecoverySource(); } void MpvPlayer::HandleMpvEvent(mpv_event* event) { if (plezy::mpv_common::DispatchReplyEvent( pending_requests_, event, [](const char* value) { return SanitizeUtf8(value); })) { return; } switch (event->event_id) { case MPV_EVENT_LOG_MESSAGE: { auto* msg = static_cast(event->data); if (!msg) break; g_message("MPV [%s] %s: %s", msg->level, msg->prefix, msg->text); FlValue* data = fl_value_new_map(); fl_value_set_string_take(data, "prefix", fl_value_new_string(SanitizeUtf8(msg->prefix).c_str())); fl_value_set_string_take(data, "level", fl_value_new_string(SanitizeUtf8(msg->level).c_str())); fl_value_set_string_take(data, "text", fl_value_new_string(SanitizeUtf8(msg->text).c_str())); SendEvent("log-message", data); fl_value_unref(data); break; } case MPV_EVENT_PROPERTY_CHANGE: { auto* prop = static_cast(event->data); if (!prop || !prop->name) break; mpv_node node = plezy::mpv_common::ExtractPropertyNode(prop); const auto notice = plezy::mpv_common::ObserveAudioRecoveryProperty(audio_recovery_, event, prop); if (notice.message) LogRecovery(notice.message); // Recovery runs off a GLib timer here, so newly queued work has to arm it. if (notice.scheduled_work) EnsureAudioRecoveryTimer(); SendPropertyChange(prop->name, &node); break; } case MPV_EVENT_END_FILE: { audio_recovery_.SetFileLoaded(false); auto* end = static_cast(event->data); if (!end) break; FlValue* data = fl_value_new_map(); fl_value_set_string_take(data, "reason", fl_value_new_int(static_cast(end->reason))); if (end->reason == MPV_END_FILE_REASON_ERROR) { fl_value_set_string_take(data, "error", fl_value_new_int(static_cast(end->error))); fl_value_set_string_take( data, "message", fl_value_new_string(SanitizeUtf8(mpv_error_string(end->error)).c_str())); } SendEvent("end-file", data); fl_value_unref(data); break; } case MPV_EVENT_START_FILE: { SendEvent("start-file"); break; } case MPV_EVENT_FILE_LOADED: { audio_recovery_.SetFileLoaded(true); EnsureAudioRecoveryTimer(); SendEvent("file-loaded"); break; } case MPV_EVENT_PLAYBACK_RESTART: { SendEvent("playback-restart"); break; } default: break; } } namespace { // Adapts the shared, bounded mpv_node walk onto GLib-owned FlValues. struct FlValueNodeBuilder { using Value = FlValue*; using ListBuilder = FlValue*; using MapBuilder = FlValue*; static Value Null() { return fl_value_new_null(); } static Value Bool(bool value) { return fl_value_new_bool(value); } static Value Int(int64_t value) { return fl_value_new_int(value); } static Value Double(double value) { return fl_value_new_float(value); } static Value String(const char* value, size_t length) { return fl_value_new_string(SanitizeUtf8(value, length).c_str()); } static ListBuilder NewList() { return fl_value_new_list(); } static void Append(ListBuilder& list, Value value) { fl_value_append_take(list, value); } static Value FinishList(ListBuilder list) { return list; } static MapBuilder NewMap() { return fl_value_new_map(); } static void Insert(MapBuilder& map, const char* key, size_t key_length, Value value) { fl_value_set_string_take(map, SanitizeUtf8(key, key_length).c_str(), value); } static Value FinishMap(MapBuilder map) { return map; } static void AbandonMap(MapBuilder& map) { fl_value_unref(map); } }; } // namespace FlValue* MpvPlayer::NodeToFlValue(mpv_node* node) { return plezy::mpv_common::ConvertNode(node); } FlValue* MpvPlayer::NodeToFlValue(mpv_node* node, plezy::mpv_common::NodeConversionBudget* budget) { return plezy::mpv_common::ConvertNode(node, 0, budget); } void MpvPlayer::SendPropertyChange(const char* name, mpv_node* data) { if (!name) return; int id = 0; if (!observed_properties_.LookupId(name, &id)) return; FlValue* list = fl_value_new_list(); fl_value_append_take(list, fl_value_new_int(id)); if (data) { fl_value_append_take(list, NodeToFlValue(data)); } else { fl_value_append_take(list, fl_value_new_null()); } EventCallback callback; { std::lock_guard lock(callback_mutex_); callback = event_callback_; } if (callback) callback(list); fl_value_unref(list); } void MpvPlayer::SendEvent(const std::string& name, FlValue* data) { FlValue* event_map = fl_value_new_map(); fl_value_set_string_take(event_map, "type", fl_value_new_string("event")); fl_value_set_string_take(event_map, "name", fl_value_new_string(name.c_str())); if (data) { fl_value_set_string_take(event_map, "data", fl_value_ref(data)); } EventCallback callback; { std::lock_guard lock(callback_mutex_); callback = event_callback_; } if (callback) callback(event_map); fl_value_unref(event_map); } void MpvPlayer::SetHDREnabled(bool enabled, StatusCallback 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