#include "mpv_plugin.h" #include #include #include #include #include #include #include "wayland_video_surface.h" using PlayerPtr = std::unique_ptr; using VideoSurfacePtr = std::unique_ptr; // One queued HDR transaction: what to apply, and who to tell when it settles. // // `mode` is engaged only when the request *is* the mode change. Everything else // leaves it empty and inherits whatever is in force at the moment its turn // comes: a mode captured at enqueue time could have been refused since, and // applying it then would put a curve on screen that Dart has already been told // did not take. struct PendingHdrRequest { bool allow = false; std::optional mode; std::function done; }; using HdrQueue = std::deque; struct _MpvPlugin { GObject parent_instance; FlPluginRegistrar* registrar; FlMethodChannel* method_channel; FlEventChannel* event_channel; PlayerPtr player; // The native Wayland plane every video instance renders into. Non-null once // initialize() has brought it up; there is no second render path, so a null // here on a video instance means initialization refused. VideoSurfacePtr video_surface; // Set when the plane must be redrawn even though mpv has no new frame - // after a resize or after becoming visible, where the buffer on screen is // stale or absent. Sticky, because the render it asks for may first have to // wait out an unacknowledged frame. gboolean plane_needs_render; gboolean visible; gboolean initialized; gboolean audio_only; // What Dart last asked for via hdr-enabled. Remembered because the answer can // change without Dart saying anything: moving the window to another monitor // changes whether the output is in HDR at all, and the plane has to be // re-described when it does. gboolean hdr_wanted; // The last geometry Dart sent, kept whether or not a plane existed to take // it. Dart sends a rect only when its numbers change, so one arriving before // start_video_plane is the only one the plane may ever be offered. struct PendingRect { int32_t x = 0; int32_t y = 0; int32_t width = 0; int32_t height = 0; int32_t scale = 1; }; PendingRect pending_rect; gboolean has_pending_rect; // Bumped on every teardown so an async HDR callback that outlived its plane // can tell it is answering for a generation that no longer exists. guint64 generation; // Who tone-maps when HDR is on. Defaults to the compositor, which is the // behaviour that shipped first and needs no knowledge of the display; the // player-side path is opted into. // // Two fields, deliberately. `hdr_tone_mapping` is the mode mpv last *accepted*; // `hdr_tone_mapping_desired` is what the app last asked for. Requests are // applied strictly in order, so an internal re-apply (playback restart, // preferred-description change) queued behind a user's mode change must carry // the desired mode - reading the committed one would send the stale mode last // and make it final. mpv::HdrToneMapping hdr_tone_mapping = mpv::HdrToneMapping::kCompositor; mpv::HdrToneMapping hdr_tone_mapping_desired = mpv::HdrToneMapping::kCompositor; // Bumped per user mode request, so a failure only reverts `desired` when no // newer request has already replaced it. uint64_t hdr_mode_request_serial = 0; // Same purpose for hdr-enabled: a refused request must only hand hdr_wanted // back if no newer one has claimed it since. uint64_t hdr_enable_request_serial = 0; // Exactly one HDR transaction runs at a time, end to end. // // A transaction spans staging and validating the image description, switching // mpv's output colour space, and committing both. Interleaving two cannot be // made consistent after the fact: a superseded transaction may already have // moved mpv, so refusing its commit leaves mpv on one curve and the surface // describing another. Serializing the whole thing is what makes that // unreachable, rather than something to detect and unwind. bool hdr_transaction_in_flight = false; // Waiting user requests, in order. Each keeps its own callback because each has // a Dart method call to answer, and answering one with another's outcome is the // same divergence one level up. HdrQueue hdr_queue; // Internal re-applies coalesce into a flag instead of queueing: they have no // caller to answer, playback-restart fires on every seek, and each transaction // re-reads the source when its turn comes, so collapsing several loses nothing. bool hdr_reapply_pending = false; // The peak mpv was last told to tone-map to, 0 meaning "do not". Compared // against the display's current peak so a move between two HDR outputs is not // mistaken for no change at all. uint32_t applied_target_peak = 0; // Set when a transaction ended in kUnknown: mpv stopped answering partway // through being put back, so what the plane emits cannot be named and the // surface carries no description. Recorded rather than inferred from the // surface being hidden, because Dart's own visibility changes move that bit // for entirely unrelated reasons and would otherwise lift the quarantine by // accident. Cleared by the next transaction that ends in a nameable state. bool hdr_output_unnameable = false; // What the source snapshot last logged as, so a seek does not repeat it. Its // default state means "no stream", which no real source matches, so the first // one always logs. Placement-constructed in init - see the note by finalize. mpv::HdrMetadata last_logged_source; }; // g_type_create_instance zeroes the instance and runs no constructor, so the // member initialisers above never execute: every scalar starts at zero and // nothing else assigns these. kCompositor has to *be* zero for that to land on // the intended default. static_assert( static_cast(mpv::HdrToneMapping::kCompositor) == 0, "MpvPlugin's zeroed instance memory must decode as kCompositor"); G_DEFINE_TYPE(MpvPlugin, mpv_plugin, G_TYPE_OBJECT) // Forward declarations static void mpv_plugin_handle_method_call(FlMethodChannel* channel, FlMethodCall* method_call, gpointer user_data); static mpv::HdrMetadata read_source_hdr_metadata(MpvPlugin* self); static void apply_hdr_state(MpvPlugin* self, bool allow, mpv::HdrToneMapping mode, std::function done); static void request_hdr_reapply(MpvPlugin* self); static void run_next_hdr_transaction(MpvPlugin* self); static void submit_hdr_transaction( MpvPlugin* self, bool allow, std::optional mode, std::function done); // Events reach Dart unchanged; this only watches them go past. A playback // restart is the first moment the source's colour space and HDR metadata are // knowable, and HDR is normally permitted well before that - typically before // any file is open - so the decision made back then has to be revisited. // // It goes through the full apply path rather than only refreshing the // description, because a new file can change the *curve*: PQ to HLG, or HDR to // SDR entirely, each of which mpv has to be reconfigured for and not just // re-described. Self-cancelling when nothing changed, which matters because this // also fires on every seek. static void observe_event_for_hdr(MpvPlugin* self, FlValue* event) { if (!self->video_surface) return; if (event == nullptr || fl_value_get_type(event) != FL_VALUE_TYPE_MAP) return; FlValue* name = fl_value_lookup_string(event, "name"); if (name == nullptr || fl_value_get_type(name) != FL_VALUE_TYPE_STRING) return; if (g_strcmp0(fl_value_get_string(name), "playback-restart") != 0) return; request_hdr_reapply(self); } static void send_event(MpvPlugin* self, FlValue* event) { observe_event_for_hdr(self, event); if (self->event_channel) { g_autoptr(GError) error = nullptr; if (!fl_event_channel_send(self->event_channel, event, nullptr, &error) && error != nullptr) { g_warning("Failed to send event: %s", error->message); } } } // Every event Dart accepts carries type:"event" - its decoder silently drops a // map without it. Building the envelope in one place means the next caller // cannot omit it and quietly get nothing. static void send_named_event(MpvPlugin* self, const char* name) { g_autoptr(FlValue) event = fl_value_new_map(); fl_value_set_string_take(event, "type", fl_value_new_string("event")); fl_value_set_string_take(event, "name", fl_value_new_string(name)); send_event(self, event); } static void release_video_resources(MpvPlugin* self) { // Anything still in flight is now answering for a plane that is going away. ++self->generation; // Queued transactions will never run, and each may be holding a reference to a // Dart method call that has to be answered or it is leaked along with its // response. auto queued = std::move(self->hdr_queue); self->hdr_queue.clear(); self->hdr_transaction_in_flight = false; self->hdr_reapply_pending = false; for (auto& request : queued) { if (request.done) request.done(MPV_ERROR_UNINITIALIZED); } if (self->player) { // The plane is a raw callback target. Revoke every callback path before // tearing it down; Dispose then drains any callback already holding a // native lease. self->player->SetRedrawCallback(nullptr); self->player->SetSourceMetadataCallback(nullptr); self->player->SetEventCallback(nullptr); self->player->Dispose(); self->player.reset(); } // After the player is gone: disposal hands the render context to the // process-lifetime teardown queue, which releases it surfacelessly, so the // plane's EGL surface does not have to outlive it. if (self->video_surface) { self->video_surface->Destroy(); self->video_surface.reset(); } self->initialized = FALSE; self->visible = FALSE; self->plane_needs_render = FALSE; // All of these describe a plane and an mpv instance that no longer exist, and // initialize() builds both fresh: a new MpvPlayer whose applied target // properties are back to "auto", and a new surface with no description // attached. Left standing, the stale record makes a repeated hdr-tone-mapping // write match the old accepted mode and answer success without applying // anything, and gives handle_preferred_changed a peak to compare against that // nothing is actually using. self->hdr_tone_mapping = mpv::HdrToneMapping::kCompositor; self->hdr_tone_mapping_desired = mpv::HdrToneMapping::kCompositor; self->applied_target_peak = 0; // The quarantine belongs to the mpv instance that stopped answering, not to // the app. A new plane and a new player have said nothing yet, so nothing // about them is unnameable, and leaving this set would hide the next session // outright: the flag suppresses exactly the setVisible that would show it. self->hdr_output_unnameable = false; self->last_logged_source = mpv::HdrMetadata(); // hdr_wanted included, and this is not obvious. It reads like the user's // permission, which outlives any one plane - but this plugin is a // process-lifetime singleton, so "outlives the plane" means "outlives every // later session too". Dart re-pushes the preference on each initialize and // reconciles a refusal by persisting what it believes native holds, and that // belief is "off, because the plane is new". Carrying a TRUE over from an // earlier session makes that belief false exactly when a refusal happens, and // the two then disagree with no way back. A fresh plane starts from no // permission and waits to be told. self->hdr_wanted = FALSE; } // Hands the plane the last geometry Dart sent. Called both when a rect arrives // and when a plane is created after one already had, which is the case that // would otherwise leave the plane sizeless: render_video_plane bails while // has_size() is false, and Dart re-sends only when its numbers change. static void apply_pending_rect(MpvPlugin* self) { if (!self->has_pending_rect || self->video_surface == nullptr) return; const auto& rect = self->pending_rect; self->video_surface->SetRect(rect.x, rect.y, rect.width, rect.height, rect.scale); } // Renders and presents one frame on the native video plane. Skipped while the // plane is hidden or has not been given a rect yet; both of those paths render // explicitly once the condition clears, because mpv's redraw latch stays set // until a render consumes it and would otherwise suppress every later frame. // // |force| is for the callers who need pixels regardless of whether mpv has // produced a new frame: a resize, or the plane becoming visible again. static void render_video_plane(MpvPlugin* self, gboolean force) { if (force) self->plane_needs_render = TRUE; if (!self->player || !self->video_surface || !self->video_surface->valid()) return; if (!self->video_surface->visible() || !self->video_surface->has_size()) return; // Present() is held while a colour transition is staged, so a render now would // be discarded. The forced render after CommitHdrTransition is what resumes; // plane_needs_render stays set meanwhile, so nothing is lost. if (self->video_surface->hdr_transition_staged()) return; // Skip entirely while the compositor has not acknowledged the last frame: // an occluded plane is never acknowledged, and rendering into it anyway // would burn GPU work on frames that can never be shown. if (self->video_surface->frame_pending()) return; // The frame callback fires once per *display* refresh, so rendering from it // unconditionally pins the plane to the monitor's rate - 120 swaps/s for // 60fps content on a 120Hz output, half of them redrawing the same picture. // mpv's redraw latch is what says a new frame actually exists. if (!self->plane_needs_render && !self->player->NeedsRedraw()) return; if (self->player->RenderToSurface( self->video_surface->egl_surface(), self->video_surface->width(), self->video_surface->height())) { // Only once a frame has actually been published. Present() returns false on // an eglSwapBuffers failure having already destroyed its frame callback, so // clearing the flag first would drop both the retry and the thing that would // have rescheduled it, and the plane would sit on a stale buffer until an // unrelated event arrived. Its other false returns are all re-tested above // on this same thread, so a swap failure is the only way to get here. if (self->video_surface->Present()) self->plane_needs_render = FALSE; } } // Collects what the source actually is, plus its HDR10 static metadata. // // Both halves matter. The colour space decides whether the plane may be // described as HDR at all — describing it because a setting is on, rather than // because the stream carries an HDR curve, asks the compositor to undo a // transform nobody applied. The luminances then decide how hard the compositor // tone-maps: without them it must assume the worst case PQ permits, 10000 nits, // and rolls highlights off far harder than the content needs. // // The protocol carries luminances as whole nits, and a value that rounds to zero // would read as "not stated", so anything positive is kept at a minimum of 1. static mpv::HdrMetadata read_source_hdr_metadata(MpvPlugin* self) { mpv::HdrMetadata metadata; if (!self->player) return metadata; mpv::SourceHdrMetadata source; if (!self->player->ReadSourceHdrMetadata(&source)) return metadata; // mpv's own trc / primaries names, from video/csputils.c's tables. if (source.transfer == "pq") { metadata.transfer = mpv::SourceTransfer::kPq; } else if (source.transfer == "hlg") { metadata.transfer = mpv::SourceTransfer::kHlg; } if (source.primaries == "bt.2020") { metadata.primaries = mpv::SourcePrimaries::kBt2020; } auto nits = [](double value) -> uint32_t { if (!(value > 0.0)) return 0; const double rounded = value + 0.5; if (rounded >= 4294967295.0) return 4294967295u; const uint32_t whole = static_cast(rounded); return whole > 0 ? whole : 1; }; metadata.max_cll = nits(source.max_cll); metadata.max_fall = nits(source.max_fall); metadata.max_luminance = nits(source.max_luminance); metadata.min_luminance = source.min_luminance; // Only when it moves. This is the input to the whole HDR decision, so it // belongs in an ordinary log - but playback-restart fires on every seek, and // an unconditional line would bury the output state it should sit next to. if (metadata != self->last_logged_source) { self->last_logged_source = metadata; g_message( "MPV video plane: source is %s / %s, MaxCLL=%u MaxFALL=%u mastering=%.4f-%u nits", source.transfer.empty() ? "(no stream)" : source.transfer.c_str(), source.primaries.empty() ? "(no stream)" : source.primaries.c_str(), metadata.max_cll, metadata.max_fall, metadata.min_luminance, metadata.max_luminance); } return metadata; } // Whether the client and the display could carry HDR at all, before the source // is considered. Both halves must agree: this client has to be able to describe // an HDR plane, and the output the surface sits on has to actually be in HDR. // The second can change under us when the window moves between monitors, which // is why nothing caches it. static bool hdr_available(MpvPlugin* self) { return self->video_surface != nullptr && self->video_surface->supports_hdr() && self->video_surface->output_is_hdr(); } // One gate, in hdr_metadata.h, so the four conditions and the peak clamp are // testable without a compositor. Both the apply path and the preferred-changed // check go through here, so an eighth HdrInputs field cannot be filled in one // and forgotten in the other - they would then disagree about what is on screen. // // The caller must have established that the surface exists. static mpv::HdrDecision decide_hdr( MpvPlugin* self, bool allow, mpv::HdrToneMapping mode, const mpv::HdrMetadata& source) { mpv::HdrInputs inputs; inputs.allowed = allow; inputs.client_can_describe = self->video_surface->supports_hdr(); inputs.output_is_hdr = self->video_surface->output_is_hdr(); inputs.source_describable = self->video_surface->CanDescribeSource(source); inputs.requested = mode; inputs.display_peak_nits = self->video_surface->preferred().max_luminance; inputs.sdr_reference_nits = self->video_surface->preferred().reference_luminance; return mpv::DecideHdr(inputs, source); } // Applies an HDR state to both halves of the plane, atomically on screen. // // The surface's colour state and the buffer it describes land on the *same* // child-surface commit, and that commit is performed by eglSwapBuffers inside // Present(). So neither "pixels first" nor "description first" is atomic on its // own: whichever goes second leaves a window in which presented frames carry one // colour space while labelled with the other. On enable that window is the // compositor's validation round-trip, and PQ frames read as sRGB are a visible // flash. // // The three-step transition closes it: // // 1. BeginHdrTransition stages and validates the description, holding Present() // so nothing can commit mid-change. // 2. Once it settles, mpv's output colour space is switched. // 3. CommitHdrTransition attaches the state and releases the hold, and the // plane is rendered and presented immediately - so the first buffer in the // new colour space is the one that carries it. // // Any failure aborts, leaving both halves exactly as they were. // // `mode` is the *desired* tone-map owner, passed in rather than read from the // plugin, so the committed field is updated only when the request carrying that // mode is the one mpv accepted. Requests are applied in order, so the last // success is what mpv holds and what gets committed last. // // The source is read once and the same snapshot drives every step. static void apply_hdr_state(MpvPlugin* self, bool allow, mpv::HdrToneMapping mode, std::function done) { if (self->video_surface == nullptr || self->player == nullptr) { if (done) done(MPV_ERROR_UNINITIALIZED); return; } const mpv::HdrMetadata source = read_source_hdr_metadata(self); const mpv::HdrDecision decision = decide_hdr(self, allow, mode, source); // What the buffer will actually contain: the source untouched, or the same // curve and gamut reduced to the peak we are about to declare. DecideHdr // already clamped that peak to the curve's primary colour volume, so mpv aims // at exactly what the compositor is told. const mpv::HdrMetadata described = decision.tone_map_in_player ? mpv::DescribeTonemappedTo(source, decision.target_peak_nits) : source; const mpv::SourceTransfer transfer = decision.describe ? described.transfer : mpv::SourceTransfer::kSdr; const guint64 generation = self->generation; self->video_surface->BeginHdrTransition( decision.describe, described, [self, decision, transfer, mode, generation, done](uint64_t token, bool staged) { if (self->generation != generation || self->video_surface == nullptr || self->player == nullptr) { if (done) done(MPV_ERROR_UNINITIALIZED); return; } if (!staged) { self->video_surface->AbortHdrTransition(token); // Releasing the hold is not enough to restart the plane. mpv's redraw // latch saturated while Present() was held - OnMpvRenderUpdate only // schedules on the false->true edge - and no frame callback is // outstanding to poke it either, so without a forced render here the // plane sits on its last buffer until something unrelated moves. render_video_plane(self, TRUE); g_warning("MPV video plane: colour transition abandoned; leaving HDR as it was"); if (done) done(MPV_ERROR_UNSUPPORTED); return; } self->player->SetHdrOutput( transfer, decision.target_peak_nits, [self, decision, mode, generation, token, done](mpv::MpvPlayer::HdrOutputResult result, int error) { using Result = mpv::MpvPlayer::HdrOutputResult; // The plane may have been torn down while the property was in flight. if (self->generation != generation || self->video_surface == nullptr || self->player == nullptr) { if (done) done(error); return; } // An earlier kUnknown hid the plane rather than show pixels it could // not label. Every outcome below except another kUnknown leaves mpv // in a state that *can* be named, so the visibility Dart actually // asked for is restored for all of them - restoring it only on // kApplied left a clean unwind showing black until some unrelated // visibility change arrived. Done before the switch so each arm's // own render publishes it. // // Driven off the recorded quarantine rather than off the surface // being hidden: those are different facts. Dart hides the plane // whenever the player is off screen, and reading that as "quarantined" // would restore visibility the user did not ask for. // // The commit is resolved first, because "mpv applied it" and "the // surface is describing it" are two facts and the quarantine cares // about the second. A kApplied whose non-zero token was refused is // the case they part company: the watchdog withdrew the description // while mpv was still answering, so mpv has moved to PQ and the // surface has nothing attached. Lifting on that would publish the // mislabelled frame the kUnknown arm below refuses to publish - and // the same slow mpv causes both halves, so they arrive together. // Short-circuit: only kApplied may commit; the other arms abort or // withdraw instead. const bool committed = result == Result::kApplied && self->video_surface->CommitHdrTransition(token); const bool nameable = result != Result::kUnknown && (result != Result::kApplied || committed || token == 0); const bool unquarantined = nameable && self->hdr_output_unnameable; if (unquarantined) { self->hdr_output_unnameable = false; if (self->visible != FALSE) self->video_surface->SetVisible(true); } switch (result) { case Result::kApplied: { // Pixels and state now agree; publish them together. if (committed || unquarantined) render_video_plane(self, TRUE); if (committed && decision.describe) { g_message( "MPV video plane: HDR on, tone mapping by %s", decision.tone_map_in_player ? "the player" : "the compositor"); } // A refusal has two very different meanings. Token zero is the // benign one: nothing needed staging because nothing changed, so // the record below is already true. A non-zero token that was // refused means the transition was torn down while this mpv leg // was in flight - the watchdog withdrew the description - and // mpv has now moved to a colour space the surface no longer // claims. Recording that as applied would make it the truth as // far as every later no-change test is concerned, including the // one in handle_preferred_changed that would otherwise repair // it. Leave the record alone and ask for a fresh apply instead. if (!committed && token != 0) { g_warning("MPV video plane: the colour transition was withdrawn mid-flight; re-applying"); request_hdr_reapply(self); break; } self->hdr_tone_mapping = mode; self->applied_target_peak = decision.target_peak_nits; break; } case Result::kRestored: // mpv is back where it was, so the description already committed // is still true of the pixels and must be left exactly alone. // The plane still has to be restarted - see the !staged arm. self->video_surface->AbortHdrTransition(token); render_video_plane(self, TRUE); g_warning( "MPV video plane: mpv refused the %s output colour space and was put back, " "so the surface description is unchanged: %s", decision.describe ? "HDR" : "SDR", mpv_error_string(error)); break; case Result::kForcedSdr: // mpv could not be put back and is now SDR. Any committed HDR // description describes pixels that no longer exist, so it goes // too - and immediately, paired with a fresh frame. The render is // unconditional because the hold is released either way. self->video_surface->ForceUndescribed(); render_video_plane(self, TRUE); self->applied_target_peak = 0; g_warning( "MPV video plane: mpv's colour space could not be restored and was forced to " "SDR; HDR withdrawn: %s", mpv_error_string(error)); break; case Result::kUnknown: // Nothing can be said truthfully about these pixels, so nothing is // said and nothing is shown. A later transaction can recover. // Recorded, so that a setVisible arriving in between - the app // going off screen and back, which has nothing to do with colour - // cannot quietly put the mislabelled plane back on screen. self->hdr_output_unnameable = true; self->video_surface->ForceUndescribed(); self->video_surface->SetVisible(false); self->applied_target_peak = 0; g_warning( "MPV video plane: mpv's output colour space is no longer commandable; the " "plane is hidden rather than shown mislabelled: %s", mpv_error_string(error)); break; } if (done) done(error); }); }); } // Runs the next queued HDR transaction, or drains the coalesced internal // re-apply once the queue empties. static void run_next_hdr_transaction(MpvPlugin* self) { if (self->hdr_queue.empty()) { self->hdr_transaction_in_flight = false; if (self->hdr_reapply_pending) { self->hdr_reapply_pending = false; request_hdr_reapply(self); } return; } PendingHdrRequest request = std::move(self->hdr_queue.front()); self->hdr_queue.pop_front(); self->hdr_transaction_in_flight = true; // Resolved here rather than at enqueue, so a mode that has since been refused // is not applied on this request's back. const mpv::HdrToneMapping mode = request.mode.value_or(self->hdr_tone_mapping_desired); apply_hdr_state(self, request.allow, mode, [self, done = std::move(request.done)](int error) { if (done) done(error); // Only now, with the surface unstaged and mpv settled, may the next one start. run_next_hdr_transaction(self); }); } // Queues an HDR transaction. `mode` is engaged only for the mode change itself; // everything else passes nullopt and carries whatever mode is in force when its // turn comes. static void submit_hdr_transaction( MpvPlugin* self, bool allow, std::optional mode, std::function done) { if (self->video_surface == nullptr || self->player == nullptr) { if (done) done(MPV_ERROR_UNINITIALIZED); return; } PendingHdrRequest request; request.allow = allow; request.mode = mode; request.done = std::move(done); self->hdr_queue.push_back(std::move(request)); if (!self->hdr_transaction_in_flight) run_next_hdr_transaction(self); } // Whether an HDR transaction is under way. A merely queued request counts as // under way and needs no separate test: submit_hdr_transaction starts one the // moment nothing is running, and run_next_hdr_transaction clears the flag only // on the branch where the queue is already empty - so a non-empty queue always // implies the flag is set. static bool hdr_busy(MpvPlugin* self) { return self->hdr_transaction_in_flight; } // The only entry point for internal re-applies: playback restarts and // preferred-description changes. Nobody is waiting on these, so instead of // queueing they collapse into a single pending flag and re-read the mode and the // source when their turn comes. static void request_hdr_reapply(MpvPlugin* self) { if (self->video_surface == nullptr) return; // Folded into the flag rather than queued behind what is already running: // starting a re-apply beside waiting user requests would put it ahead of them // in effect, and each transaction re-reads the source and the preferred // description when its turn comes, so coalescing loses nothing. if (hdr_busy(self)) { self->hdr_reapply_pending = true; return; } submit_hdr_transaction(self, self->hdr_wanted != FALSE, std::nullopt, nullptr); } // Re-applies the current request when the compositor's preferred description // moves - a monitor change, HDR switched on or off under the app, or the output // going away entirely. static void handle_preferred_changed(MpvPlugin* self) { if (self->video_surface == nullptr) return; // Dart mirrors the HDR controls off isHDRSupported, and that answer depends on // which output the surface sits on. This is the only place that learns it // moved: dragging a window between monitors raises no Flutter lifecycle event // on Wayland, so without this the settings sheet goes on offering a toggle the // output can no longer honour, or hiding one it now could. Sent before the // busy check below, because a deferred re-apply is still a change Dart needs // to hear about. send_named_event(self, "hdr-output-changed"); // A transaction that has not committed yet has not moved hdr_active() or // applied_target_peak, so comparing against them here would judge the new // preference against the state the in-flight transaction is about to // replace. When the two happen to match, the newest word from the compositor // would be dropped rather than coalesced - which is reachable just by // dragging the window across two monitors, where KWin emits several // preferred_changed events and the intermediate one can carry no_output. // Defer instead; request_hdr_reapply already coalesces, and apply_hdr_state // re-reads the preferred description when its turn comes. if (hdr_busy(self)) { request_hdr_reapply(self); return; } // Ask the same gate that would run anyway what the answer is now, and only // re-apply when it differs from what is in force. The on/off state alone is not // enough: in player mode the peak we tone-map to *is* the display's peak, so // moving between two HDR outputs of different brightness changes what must be // sent while the boolean stays put. const mpv::HdrMetadata source = read_source_hdr_metadata(self); const mpv::HdrDecision decision = decide_hdr(self, self->hdr_wanted != FALSE, self->hdr_tone_mapping_desired, source); if (decision.describe == self->video_surface->hdr_active() && decision.target_peak_nits == self->applied_target_peak) { return; } g_message("MPV video plane: preferred description changed; re-evaluating HDR"); request_hdr_reapply(self); } // Brings up the native Wayland video plane, the only way this runner renders // video. Returns false with |error| set to the specific reason: there is no // second path to fall through to, so the reason is what the user is told. static gboolean start_video_plane(MpvPlugin* self, FlView* view, std::string* error) { if (view == nullptr) { *error = "The window has no Flutter view to place a video plane under"; return FALSE; } GtkWidget* widget = GTK_WIDGET(view); if (!mpv::WaylandVideoSurface::IsSupported(gtk_widget_get_display(widget))) { *error = "Video needs a Wayland session. This looks like an X11 session; log in " "under Wayland, or run X11 applications through XWayland instead."; return FALSE; } // my_application.cc gives the toplevel an RGBA visual because the plane is // stacked *below* it and only shows through an alpha channel. If that did // not take, presenting here would put video behind an opaque surface: the // video area would go blank while every other symptom looked healthy. Say so // instead, because the symptom on its own points nowhere near here. GtkWidget* toplevel = gtk_widget_get_toplevel(widget); if (toplevel != nullptr && gtk_widget_is_toplevel(toplevel)) { GdkScreen* screen = gtk_widget_get_screen(toplevel); GdkVisual* rgba = screen != nullptr ? gdk_screen_get_rgba_visual(screen) : nullptr; if (rgba == nullptr || gtk_widget_get_visual(toplevel) != rgba) { *error = "The compositor gave the window no alpha channel, so a video plane below it could never be seen"; return FALSE; } } auto surface = std::make_unique(); if (!surface->Create(widget, error)) return FALSE; if (!self->player->InitRenderContextForSurface( surface->egl_display(), surface->egl_config(), surface->egl_surface(), surface->depth_bits())) { surface->Destroy(); *error = "The GPU driver would not create a render context for the video plane"; return FALSE; } self->video_surface = std::move(surface); self->video_surface->SetFrameCallback([self]() { render_video_plane(self, FALSE); }); self->video_surface->SetForcedRenderCallback([self]() { render_video_plane(self, TRUE); }); self->video_surface->SetPreferredChangedCallback([self]() { handle_preferred_changed(self); }); self->player->SetRedrawCallback([self]() { render_video_plane(self, FALSE); }); // playback-restart is not ordered against the video reconfigure that gives the // source its colour space, so the re-apply observe_event_for_hdr asks for can // land on the previous file's metadata - or on none at all for the first file. // The parse's own event therefore re-applies as well; request_hdr_reapply // coalesces the two into one transaction when they arrive together, and a late // parse converges rather than leaving a wrong description standing. self->player->SetSourceMetadataCallback([self]() { request_hdr_reapply(self); }); // A rect that arrived before this plane existed is the only one Dart may ever // offer, since it re-sends solely on change. Hand it over now, before the // first frame, so the plane is never left sizeless and blank. apply_pending_rect(self); return TRUE; } static void mpv_plugin_dispose(GObject* object) { MpvPlugin* self = MPV_PLUGIN(object); release_video_resources(self); 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); } // GObject instances come from g_type_create_instance, which zeroes the memory and // runs no C++ constructors, and are released without running destructors. Every // non-trivial member therefore has to be placement-constructed here and destroyed // in finalize, in reverse. // // A zeroed std::unique_ptr happens to behave like an empty one, which is why the // two smart pointers survived without this; a zeroed std::deque does not - its // internal map pointers must be initialised before the first push_back, or it // dereferences null. static void mpv_plugin_finalize(GObject* object) { MpvPlugin* self = MPV_PLUGIN(object); self->hdr_queue.~HdrQueue(); self->video_surface.~VideoSurfacePtr(); 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(); new (&self->video_surface) VideoSurfacePtr(); new (&self->hdr_queue) HdrQueue(); // Its default member initialisers make it non-trivially-default-constructible, // so the zeroed storage is not yet an object even though every field is scalar. // Trivially destructible, so finalize has nothing to undo. new (&self->last_logged_source) mpv::HdrMetadata(); self->visible = FALSE; self->initialized = FALSE; self->audio_only = FALSE; self->generation = 0; } MpvPlugin* mpv_plugin_new(FlPluginRegistrar* registrar, const gchar* channel_name, gboolean audio_only) { MpvPlugin* self = MPV_PLUGIN(g_object_new(MPV_PLUGIN_TYPE, nullptr)); self->registrar = FL_PLUGIN_REGISTRAR(g_object_ref(registrar)); self->audio_only = audio_only; self->player = std::make_unique(audio_only); g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new(); self->method_channel = fl_method_channel_new(fl_plugin_registrar_get_messenger(registrar), channel_name, FL_METHOD_CODEC(codec)); fl_method_channel_set_method_call_handler(self->method_channel, mpv_plugin_handle_method_call, self, nullptr); g_autofree gchar* event_channel_name = g_strconcat(channel_name, "/events", nullptr); self->event_channel = fl_event_channel_new(fl_plugin_registrar_get_messenger(registrar), event_channel_name, FL_METHOD_CODEC(codec)); return self; } // Static references to keep the plugin instances alive. static MpvPlugin* g_mpv_plugin = nullptr; static MpvPlugin* g_mpv_audio_plugin = nullptr; void mpv_plugin_register_with_registrar(FlPluginRegistrar* registrar) { g_mpv_plugin = mpv_plugin_new(registrar, "com.plezy/mpv_player", FALSE); } void mpv_audio_plugin_register_with_registrar(FlPluginRegistrar* registrar) { g_mpv_audio_plugin = mpv_plugin_new(registrar, "com.plezy/mpv_audio_player", TRUE); } /// Method call handler. static void mpv_plugin_handle_method_call(FlMethodChannel* channel, FlMethodCall* method_call, gpointer user_data) { (void)channel; MpvPlugin* self = MPV_PLUGIN(user_data); const gchar* method = fl_method_call_get_name(method_call); FlValue* args = fl_method_call_get_args(method_call); g_autoptr(FlMethodResponse) response = nullptr; if (strcmp(method, "initialize") == 0) { if (self->audio_only) { // Audio-only music core: no plane, no render context - mpv runs with // video disabled entirely (see MpvPlayer). if (!self->initialized) { if (!self->player || self->player->IsDisposed()) { self->player = std::make_unique(/*audio_only=*/true); } if (self->player->Initialize()) { self->player->SetEventCallback([self](FlValue* event) { send_event(self, event); }); self->initialized = TRUE; } } if (self->initialized) { response = FL_METHOD_RESPONSE(fl_method_success_response_new(fl_value_new_bool(TRUE))); } else { response = FL_METHOD_RESPONSE(fl_method_error_response_new("INIT_FAILED", "Failed to initialize MPV player", nullptr)); } } else if (self->video_surface && self->video_surface->valid()) { response = FL_METHOD_RESPONSE(fl_method_success_response_new(fl_value_new_bool(TRUE))); } else { if (!self->player || self->player->IsDisposed()) { self->player = std::make_unique(); } std::string error; if (!self->player->Initialize()) { release_video_resources(self); response = FL_METHOD_RESPONSE(fl_method_error_response_new("INIT_FAILED", "Failed to initialize MPV player", nullptr)); } else if (start_video_plane(self, fl_plugin_registrar_get_view(self->registrar), &error)) { ++self->generation; self->player->SetEventCallback([self](FlValue* event) { send_event(self, event); }); self->initialized = TRUE; response = FL_METHOD_RESPONSE(fl_method_success_response_new(fl_value_new_bool(TRUE))); } else { // There is no second render path. Refuse with the reason rather than // presenting into something the user cannot see. g_warning("MPV: no video plane: %s", error.c_str()); release_video_resources(self); response = FL_METHOD_RESPONSE(fl_method_error_response_new("VIDEO_PLANE_UNSUPPORTED", error.c_str(), nullptr)); } } } else if (strcmp(method, "dispose") == 0) { release_video_resources(self); response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } else if (strcmp(method, "command") == 0) { if (!self->player || !self->initialized) { response = FL_METHOD_RESPONSE(fl_method_error_response_new("NOT_INITIALIZED", "Player not initialized", nullptr)); } else { FlValue* args_value = fl_value_lookup_string(args, "args"); if (args_value == nullptr || fl_value_get_type(args_value) != FL_VALUE_TYPE_LIST) { response = FL_METHOD_RESPONSE(fl_method_error_response_new("INVALID_ARGS", "Missing 'args' list", nullptr)); } else { std::vector command_args; size_t len = fl_value_get_length(args_value); for (size_t i = 0; i < len; i++) { FlValue* item = fl_value_get_list_value(args_value, i); if (fl_value_get_type(item) == FL_VALUE_TYPE_STRING) { command_args.push_back(fl_value_get_string(item)); } } g_object_ref(method_call); self->player->CommandAsync(command_args, [method_call](int error) { g_autoptr(FlMethodResponse) async_response = nullptr; if (error < 0) { async_response = FL_METHOD_RESPONSE(fl_method_error_response_new("COMMAND_FAILED", "MPV command failed", nullptr)); } else { async_response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } fl_method_call_respond(method_call, async_response, nullptr); g_object_unref(method_call); }); return; // Response sent asynchronously } } } else if (strcmp(method, "setProperty") == 0) { if (!self->player || !self->initialized) { 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"); if (name_value == nullptr || fl_value_get_type(name_value) != FL_VALUE_TYPE_STRING) { response = FL_METHOD_RESPONSE(fl_method_error_response_new("INVALID_ARGS", "Missing 'name'", nullptr)); } else if (value_value == nullptr || fl_value_get_type(value_value) != FL_VALUE_TYPE_STRING) { response = FL_METHOD_RESPONSE(fl_method_error_response_new("INVALID_ARGS", "Missing 'value'", nullptr)); } else if (self->video_surface && g_strcmp0(fl_value_get_string(name_value), "hdr-tone-mapping") == 0) { // Not an mpv property: it selects which side reduces the source's range, // which changes both mpv's target-peak and the luminances the compositor // is told. Re-applied immediately so the switch is visible without a // seek. // // Unknown values are rejected rather than folded into the default. This // knob's whole purpose is A/B comparison, and silently answering a typo // with "compositor, success" would mislabel the very measurement it // exists to produce. const char* mode = fl_value_get_string(value_value); const bool is_player = g_strcmp0(mode, "player") == 0; const bool is_compositor = g_strcmp0(mode, "compositor") == 0; if (!is_player && !is_compositor) { response = FL_METHOD_RESPONSE(fl_method_error_response_new( "INVALID_ARGS", "hdr-tone-mapping must be 'compositor' or 'player'", nullptr)); } else { const mpv::HdrToneMapping requested = is_player ? mpv::HdrToneMapping::kPlayer : mpv::HdrToneMapping::kCompositor; // A no-op answer is only honest once the mode has actually settled. If a // change is queued or running, `desired` holds a value mpv has not yet // accepted, and answering a duplicate with immediate success would have // Dart persist a mode the original request may still revert. Such a // duplicate is queued instead and gets a real outcome; the extra // property writes are idempotent. if (requested != self->hdr_tone_mapping || hdr_busy(self)) { // `desired` moves now, so an internal re-apply that runs later carries // the new mode. `hdr_tone_mapping` itself is committed by the // transaction only if mpv accepts the change, which is what keeps // native and Dart - which does not persist on failure - in agreement. self->hdr_tone_mapping_desired = requested; const uint64_t serial = ++self->hdr_mode_request_serial; g_object_ref(method_call); submit_hdr_transaction(self, self->hdr_wanted != FALSE, requested, [self, method_call, serial](int error) { g_autoptr(FlMethodResponse) async_response = nullptr; if (plezy::mpv_common::SetPropertyStatusSucceeded(error)) { async_response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } else { // Hand the desire back to whatever is actually in force, read // live rather than captured: an intervening request may have // committed since. Skipped if a newer request already claimed // the desire. if (self->hdr_mode_request_serial == serial) { self->hdr_tone_mapping_desired = self->hdr_tone_mapping; } async_response = FL_METHOD_RESPONSE(fl_method_error_response_new( plezy::mpv_common::kSetPropertyFailedCode, mpv_error_string(error), nullptr)); } fl_method_call_respond(method_call, async_response, nullptr); g_object_unref(method_call); }); return; } response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } } else if (self->video_surface && g_strcmp0(fl_value_get_string(name_value), "hdr-enabled") == 0) { // HDR spans both halves of the plane: mpv has to emit PQ / BT.2020, and // the compositor has to be told that is what the buffer holds. Neither // alone produces HDR, so this cannot go through the plain property path. const bool enabled = plezy::mpv_common::ParseEnabledFlag(fl_value_get_string(value_value)); // What every internal re-apply reads. This records the user's // permission, which is app policy and not a capability, so it is kept // even when the output cannot show HDR right now: decide_hdr gates on // output_is_hdr separately, and the whole point of hdr_wanted is that // the plane can be re-described when the window reaches an HDR output. // Rolling it back on a temporarily-SDR output would strand the session // permanently SDR while Dart went on believing HDR was enabled - it // swallows this error and keeps the setting persisted. // // A plane that can never describe HDR is a different matter. That is // fixed for the session - an 8-bit config, or a compositor without the // colour-management pieces - so refusing is honest and Dart can say so. const gboolean previous_wanted = self->hdr_wanted; self->hdr_wanted = enabled ? TRUE : FALSE; if (enabled && !(self->video_surface->supports_hdr())) { self->hdr_wanted = previous_wanted; response = FL_METHOD_RESPONSE(fl_method_error_response_new( "HDR_UNSUPPORTED", "This compositor or video plane cannot carry HDR", nullptr)); } else { g_object_ref(method_call); const uint64_t serial = ++self->hdr_enable_request_serial; submit_hdr_transaction(self, enabled, std::nullopt, [self, method_call, serial, previous_wanted](int error) { g_autoptr(FlMethodResponse) async_response = nullptr; if (plezy::mpv_common::SetPropertyStatusSucceeded(error)) { async_response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } else { // Only if no newer request has claimed the field since, for the // same reason the tone-mapping path checks its serial. if (self->hdr_enable_request_serial == serial) self->hdr_wanted = previous_wanted; async_response = FL_METHOD_RESPONSE(fl_method_error_response_new( plezy::mpv_common::kSetPropertyFailedCode, mpv_error_string(error), nullptr)); } fl_method_call_respond(method_call, async_response, nullptr); g_object_unref(method_call); }); return; } } else { 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 = nullptr; if (plezy::mpv_common::SetPropertyStatusSucceeded(error)) { async_response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } else { const char* error_code = plezy::mpv_common::SetPropertyErrorCode(error); const std::string description = error == MPV_ERROR_UNINITIALIZED ? std::string("Player not initialized") : plezy::mpv_common::SetPropertyErrorDescription(error); async_response = FL_METHOD_RESPONSE(fl_method_error_response_new(error_code, description.c_str(), nullptr)); } fl_method_call_respond(method_call, async_response, nullptr); g_object_unref(method_call); }); return; // Response sent asynchronously } } } else if (strcmp(method, "setLogLevel") == 0) { if (!self->player || !self->initialized) { response = FL_METHOD_RESPONSE(fl_method_error_response_new("NOT_INITIALIZED", "Player not initialized", nullptr)); } else { FlValue* level_value = fl_value_lookup_string(args, "level"); if (level_value == nullptr || fl_value_get_type(level_value) != FL_VALUE_TYPE_STRING) { response = FL_METHOD_RESPONSE(fl_method_error_response_new("INVALID_ARGS", "Missing 'level'", nullptr)); } else { self->player->SetLogLevel(fl_value_get_string(level_value)); response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } } } else if (strcmp(method, "getProperty") == 0) { if (!self->player || !self->initialized) { response = FL_METHOD_RESPONSE(fl_method_error_response_new("NOT_INITIALIZED", "Player not initialized", nullptr)); } else { FlValue* name_value = fl_value_lookup_string(args, "name"); if (name_value == nullptr || fl_value_get_type(name_value) != FL_VALUE_TYPE_STRING) { response = FL_METHOD_RESPONSE(fl_method_error_response_new("INVALID_ARGS", "Missing 'name'", nullptr)); } else { g_object_ref(method_call); self->player->GetPropertyAsync( fl_value_get_string(name_value), [method_call](int error, const std::string& value) { g_autoptr(FlMethodResponse) async_response = nullptr; if (error < 0 || value.empty()) { async_response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } else { async_response = FL_METHOD_RESPONSE(fl_method_success_response_new(fl_value_new_string(value.c_str()))); } fl_method_call_respond(method_call, async_response, nullptr); g_object_unref(method_call); }); return; // Response sent asynchronously } } } else if (strcmp(method, "observeProperty") == 0) { if (!self->player || !self->initialized) { response = FL_METHOD_RESPONSE(fl_method_error_response_new("NOT_INITIALIZED", "Player not initialized", nullptr)); } else { FlValue* name_value = fl_value_lookup_string(args, "name"); FlValue* format_value = fl_value_lookup_string(args, "format"); FlValue* id_value = fl_value_lookup_string(args, "id"); if (name_value == nullptr || fl_value_get_type(name_value) != FL_VALUE_TYPE_STRING) { response = FL_METHOD_RESPONSE(fl_method_error_response_new("INVALID_ARGS", "Missing 'name'", nullptr)); } else if (format_value == nullptr || fl_value_get_type(format_value) != FL_VALUE_TYPE_STRING) { response = FL_METHOD_RESPONSE(fl_method_error_response_new("INVALID_ARGS", "Missing 'format'", nullptr)); } else if (id_value == nullptr || fl_value_get_type(id_value) != FL_VALUE_TYPE_INT) { response = FL_METHOD_RESPONSE(fl_method_error_response_new("INVALID_ARGS", "Missing 'id'", nullptr)); } else { self->player->ObserveProperty( fl_value_get_string(name_value), fl_value_get_string(format_value), static_cast(fl_value_get_int(id_value))); response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } } } else if (strcmp(method, "setVisible") == 0) { FlValue* visible_value = fl_value_lookup_string(args, "visible"); if (visible_value == nullptr || fl_value_get_type(visible_value) != FL_VALUE_TYPE_BOOL) { response = FL_METHOD_RESPONSE(fl_method_error_response_new("INVALID_ARGS", "Missing 'visible'", nullptr)); } else { self->visible = fl_value_get_bool(visible_value); if (self->video_surface) { // Hiding is always Dart's to do. Showing is not, while the plane is // quarantined: the description was withdrawn because what mpv emits // could not be named, and showing it now would be the mislabelled // picture the kUnknown arm just refused. Dart's wish is still recorded // above, so the transaction that lifts the quarantine honours it. if (self->visible && self->hdr_output_unnameable) { // Actively, rather than waiting for an unrelated event: coming back on // screen is exactly when it is worth asking mpv again, and the // transaction either names the output and unhides, or lands on // kUnknown again and leaves things as they are. request_hdr_reapply(self); } else { self->video_surface->SetVisible(self->visible); // Becoming visible has to render explicitly: the redraw latch was // consumed (or suppressed) while hidden, so no callback is pending. if (self->visible) render_video_plane(self, TRUE); } } response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } } else if (strcmp(method, "isHDRSupported") == 0) { // The output half of the gate is what stops the app offering an HDR toggle // on an SDR panel, where enabling it only invites the compositor to tone-map // a plane that never needed to be PQ in the first place. response = FL_METHOD_RESPONSE(fl_method_success_response_new(fl_value_new_bool(hdr_available(self)))); } else if (strcmp(method, "setVideoRect") == 0) { { auto read_int = [args](const char* key, int64_t* out) { FlValue* value = fl_value_lookup_string(args, key); if (value == nullptr || fl_value_get_type(value) != FL_VALUE_TYPE_INT) return false; *out = fl_value_get_int(value); return true; }; int64_t left = 0, top = 0, right = 0, bottom = 0; if (!read_int("left", &left) || !read_int("top", &top) || !read_int("right", &right) || !read_int("bottom", &bottom)) { response = FL_METHOD_RESPONSE(fl_method_error_response_new("INVALID_ARGS", "Missing video rect bounds", nullptr)); } else { FlValue* dpr_value = fl_value_lookup_string(args, "devicePixelRatio"); double dpr = 1.0; if (dpr_value != nullptr && fl_value_get_type(dpr_value) == FL_VALUE_TYPE_FLOAT) { dpr = fl_value_get_float(dpr_value); } // GTK3 only ever reports integer scale factors, and the rect already // arrives in physical pixels, so the scale is purely how many buffer // pixels make up one surface-local unit. // // Clamped before the cast, not after: a double->int32 conversion whose // truncated value does not fit is undefined, and the two architectures // disagree about what falls out - x86-64 gives INT32_MIN, which the // lower bound below would catch, while AArch64 saturates to INT32_MAX, // which it would not. That value then becomes SetRect's rounding block // and would be sent as the buffer scale. NaN fails both comparisons and // takes the default. if (!(dpr >= 1.0)) dpr = 1.0; if (dpr > 16.0) dpr = 16.0; const int32_t scale = static_cast(dpr + 0.5); // Same reasoning as the scale above, applied to the bounds: these are // int64 channel arguments, so `right - left` can overflow before the // narrowing, and the narrowing itself is implementation-defined before // C++20. Clamp into int32 first and take the width in 64 bits, so a // hostile rect becomes a large plane rather than undefined behaviour. constexpr int64_t kMin = std::numeric_limits::min(); constexpr int64_t kMax = std::numeric_limits::max(); auto clamp32 = [](int64_t value) -> int64_t { return value < kMin ? kMin : (value > kMax ? kMax : value); }; left = clamp32(left); top = clamp32(top); const int64_t width = clamp32(clamp32(right) - left); const int64_t height = clamp32(clamp32(bottom) - top); // Remembered rather than dropped when there is no plane yet. A video // session is legitimately asked for geometry between construction and // start_video_plane, and Dart only re-sends a rect whose numbers // changed - so a rect discarded here is one the plane may never hear // again, leaving it sizeless and blank. The audio-only core keeps this // too and simply never reads it. self->pending_rect.x = static_cast(left); self->pending_rect.y = static_cast(top); self->pending_rect.width = static_cast(width); self->pending_rect.height = static_cast(height); self->pending_rect.scale = scale; self->has_pending_rect = TRUE; if (self->video_surface) { apply_pending_rect(self); // Re-render at the new size straight away; waiting for the next mpv // frame would leave a stale buffer stretched across the new rect. render_video_plane(self, TRUE); } response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } } } else if (strcmp(method, "updateFrame") == 0) { // The cross-platform "kick the video output" call. On the plane that means // forcing a render: mpv may have no new frame, but the caller is asking // because what is on screen is stale. if (self->visible) render_video_plane(self, TRUE); response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr)); } else if (strcmp(method, "isInitialized") == 0) { gboolean initialized = self->player && self->initialized; response = FL_METHOD_RESPONSE(fl_method_success_response_new(fl_value_new_bool(initialized))); } else { response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()); } fl_method_call_respond(method_call, response, nullptr); }