JJ_MKVMaker/packages/webview_cef/windows/webview_cef_plugin.cpp

500 lines
19 KiB
C++

#include "webview_cef_plugin.h"
#include "webview_cef_keyevent.h"
#ifdef WEBVIEW_CEF_GPU_TEXTURE
#include "webview_cef_gpu_texture.h"
#endif
// This must be included before many other Windows headers.
#include <windows.h>
#include <imm.h>
#include <commctrl.h>
// For getPlatformVersion; remove unless needed for your plugin implementation.
#include <VersionHelpers.h>
#include <flutter/method_channel.h>
#include <flutter_windows.h>
#include <flutter/plugin_registrar_windows.h>
#include <flutter/standard_method_codec.h>
#include <memory>
#include <thread>
#include <iostream>
#include <mutex>
#include <atomic>
#include <chrono>
#include <vector>
namespace webview_cef {
class WebviewTextureRenderer : public WebviewTexture{
public:
WebviewTextureRenderer(FlutterDesktopTextureRegistrarRef texture_registrar) {
registrar_ = texture_registrar;
texture = std::make_unique<flutter::TextureVariant>(
flutter::PixelBufferTexture([this](size_t width, size_t height) -> const FlutterDesktopPixelBuffer* {
return this->CopyPixelBuffer(width, height);
}));
FlutterDesktopTextureInfo info = {};
info.type = kFlutterDesktopPixelBufferTexture;
info.pixel_buffer_config.user_data = std::get_if<flutter::PixelBufferTexture>(texture.get());
info.pixel_buffer_config.callback = [](size_t width, size_t height, void* user_data) -> const FlutterDesktopPixelBuffer* {
auto texture = static_cast<flutter::PixelBufferTexture*>(user_data);
return texture->CopyPixelBuffer(width, height);
};
textureId = FlutterDesktopTextureRegistrarRegisterExternalTexture(registrar_, &info);
}
virtual ~WebviewTextureRenderer() {
std::lock_guard<std::mutex> autolock(mutex_);
if(registrar_){
// FlutterDesktopTextureRegistrarUnregisterExternalTexture(registrar_, textureId, nullptr, nullptr);
}
}
const FlutterDesktopPixelBuffer *CopyPixelBuffer(size_t width, size_t height) const{
std::lock_guard<std::mutex> autolock(mutex_);
return pixel_buffer.get();
}
virtual void onFrame(const void* buffer, int width, int height) override{
const std::lock_guard<std::mutex> autolock(mutex_);
if (!pixel_buffer.get() || pixel_buffer.get()->width != width || pixel_buffer.get()->height != height) {
if (!pixel_buffer.get()) {
pixel_buffer = std::make_unique<FlutterDesktopPixelBuffer>();
pixel_buffer->release_context = nullptr;
}
pixel_buffer->width = width;
pixel_buffer->height = height;
const auto size = width * height * 4;
backing_pixel_buffer.reset(new uint8_t[size]);
pixel_buffer->buffer = backing_pixel_buffer.get();
}
SwapBufferFromBgraToRgba((void*)pixel_buffer->buffer, buffer, width, height);
if(registrar_){
FlutterDesktopTextureRegistrarMarkExternalTextureFrameAvailable(registrar_, textureId);
}
}
FlutterDesktopTextureRegistrarRef registrar_;
std::unique_ptr<flutter::TextureVariant> texture;
mutable std::shared_ptr<FlutterDesktopPixelBuffer> pixel_buffer;
std::unique_ptr<uint8_t> backing_pixel_buffer;
mutable std::mutex mutex_;
};
static flutter::EncodableValue encode_wvalue_to_flvalue(WValue* args) {
// A null value (or a Null-typed WValue) maps to a null EncodableValue.
// Note: EncodableValue(nullptr) must NOT be used — under C++20 it resolves
// to std::string(const char*=nullptr) and crashes in strlen.
if (args == nullptr) {
return flutter::EncodableValue();
}
WValueType type = webview_value_get_type(args);
switch(type){
case Webview_Value_Type_Bool:
return flutter::EncodableValue(webview_value_get_bool(args));
case Webview_Value_Type_Int:
return flutter::EncodableValue(webview_value_get_int(args));
case Webview_Value_Type_Float:
// flutter::EncodableValue has no scalar float alternative; C++20's
// stricter std::variant rules no longer auto-promote float to double.
return flutter::EncodableValue(static_cast<double>(webview_value_get_float(args)));
case Webview_Value_Type_Double:
return flutter::EncodableValue(webview_value_get_double(args));
case Webview_Value_Type_String:
return flutter::EncodableValue(webview_value_get_string(args));
case Webview_Value_Type_Uint8_List: {
const uint8_t* data = webview_value_get_uint8_list(args);
return flutter::EncodableValue(std::vector<uint8_t>(data, data + webview_value_get_len(args)));
}
case Webview_Value_Type_Int32_List: {
const int32_t* data = webview_value_get_int32_list(args);
return flutter::EncodableValue(std::vector<int32_t>(data, data + webview_value_get_len(args)));
}
case Webview_Value_Type_Int64_List: {
const int64_t* data = webview_value_get_int64_list(args);
return flutter::EncodableValue(std::vector<int64_t>(data, data + webview_value_get_len(args)));
}
case Webview_Value_Type_Float_List: {
const float* data = webview_value_get_float_list(args);
return flutter::EncodableValue(std::vector<float>(data, data + webview_value_get_len(args)));
}
case Webview_Value_Type_Double_List: {
const double* data = webview_value_get_double_list(args);
return flutter::EncodableValue(std::vector<double>(data, data + webview_value_get_len(args)));
}
case Webview_Value_Type_List:
{
flutter::EncodableList ret;
size_t len = webview_value_get_len(args);
for (size_t i = 0; i < len; i++) {
ret.push_back(encode_wvalue_to_flvalue(webview_value_get_list_value(args, i)));
}
return ret;
}
case Webview_Value_Type_Map:
{
flutter::EncodableMap ret;
size_t len = webview_value_get_len(args);
for (size_t i = 0; i < len; i++) {
ret[encode_wvalue_to_flvalue(webview_value_get_key(args, i))] = encode_wvalue_to_flvalue(webview_value_get_value(args, i));
}
return ret;
}
default:
return flutter::EncodableValue();
}
}
static WValue *encode_flvalue_to_wvalue(flutter::EncodableValue* args) {
size_t index = args->index();
if (index == 1) {
return webview_value_new_bool(*std::get_if<bool>(args));
}
else if (index == 2 || index == 3) {
return webview_value_new_int(*std::get_if<int32_t>(args));
}
else if (index == 4) {
return webview_value_new_double(*std::get_if<double>(args));
}
else if (index == 5) {
return webview_value_new_string((*std::get_if<std::string>(args)).c_str());
}
else if (index == 6) {
auto list = *std::get_if<std::vector<uint8_t>>(args);
return webview_value_new_uint8_list(list.data(), list.size());
}
else if (index == 7) {
auto list = *std::get_if<std::vector<int32_t>>(args);
return webview_value_new_int32_list(list.data(), list.size());
}
else if (index == 8) {
auto list = *std::get_if<std::vector<int64_t>>(args);
return webview_value_new_int64_list(list.data(), list.size());
}
else if (index == 9) {
auto list = *std::get_if<std::vector<double>>(args);
return webview_value_new_double_list(list.data(), list.size());
}
else if (index == 10) {
WValue * ret = webview_value_new_list();
flutter::EncodableList list = *std::get_if<flutter::EncodableList>(args);
for (size_t i = 0; i < list.size(); i++) {
WValue *value = encode_flvalue_to_wvalue(&list[i]);
webview_value_append(ret, value);
webview_value_unref(value);
}
return ret;
}
else if (index == 11) {
WValue * ret = webview_value_new_map();
flutter::EncodableMap map = *std::get_if<flutter::EncodableMap>(args);
for (flutter::EncodableMap::iterator it = map.begin(); it != map.end(); it++)
{
WValue *key = encode_flvalue_to_wvalue(const_cast<flutter::EncodableValue *>(&it->first));
WValue *value = encode_flvalue_to_wvalue(const_cast<flutter::EncodableValue*>(&it->second));
webview_value_set(ret, key, value);
webview_value_unref(key);
webview_value_unref(value);
}
return ret;
}
else if (index == 12) {
return nullptr;
}
else if (index == 13) {
auto list = *std::get_if<std::vector<float>>(args);
return webview_value_new_float_list(list.data(), list.size());
}
return nullptr;
}
std::unordered_map<HWND, std::shared_ptr<WebviewPlugin>> webviewPlugins;
std::unordered_map<HWND, std::function<void(std::string method,flutter::EncodableValue * arguments)>> webviewChannels;
// Guards webviewPlugins against the vsync driver thread's snapshot reads.
static std::mutex g_pluginsMutex;
#ifdef WEBVIEW_CEF_GPU_TEXTURE
// ---- Vsync-driven external BeginFrame -----------------------------------
// With external_begin_frame_enabled the browser only produces a frame when
// SendExternalBeginFrame is called. We tick it on every display vblank so the
// webview's frame rate follows the monitor's actual refresh rate (adaptive,
// not capped at 60). WaitForVBlank blocks until the next vblank and tracks
// the current refresh rate automatically.
static std::thread g_vsyncThread;
static std::atomic<bool> g_vsyncRunning{false};
static Microsoft::WRL::ComPtr<IDXGIOutput> AcquirePrimaryDxgiOutput() {
Microsoft::WRL::ComPtr<IDXGIFactory1> factory;
if (FAILED(CreateDXGIFactory1(IID_PPV_ARGS(&factory)))) return nullptr;
Microsoft::WRL::ComPtr<IDXGIAdapter1> adapter;
if (FAILED(factory->EnumAdapters1(0, &adapter))) return nullptr;
Microsoft::WRL::ComPtr<IDXGIOutput> output;
if (FAILED(adapter->EnumOutputs(0, &output))) return nullptr;
return output;
}
static UINT QueryRefreshIntervalMs() {
DEVMODE dm = {};
dm.dmSize = sizeof(dm);
if (EnumDisplaySettings(nullptr, ENUM_CURRENT_SETTINGS, &dm) && dm.dmDisplayFrequency > 1) {
return (std::max)(UINT(1), UINT(1000 / dm.dmDisplayFrequency));
}
return 16; // assume ~60 Hz
}
static void VsyncThreadProc() {
Microsoft::WRL::ComPtr<IDXGIOutput> output = AcquirePrimaryDxgiOutput();
const UINT fallbackMs = QueryRefreshIntervalMs();
while (g_vsyncRunning) {
if (output) {
if (FAILED(output->WaitForVBlank())) {
output.Reset();
}
}
if (!output) {
std::this_thread::sleep_for(std::chrono::milliseconds(fallbackMs));
output = AcquirePrimaryDxgiOutput();
}
std::vector<std::shared_ptr<WebviewPlugin>> snapshot;
{
std::lock_guard<std::mutex> lock(g_pluginsMutex);
snapshot.reserve(webviewPlugins.size());
for (auto& kv : webviewPlugins) snapshot.push_back(kv.second);
}
for (auto& p : snapshot) {
if (p) p->tickBeginFrame();
}
}
}
static void StartVsyncDriver() {
bool expected = false;
if (g_vsyncRunning.compare_exchange_strong(expected, true)) {
g_vsyncThread = std::thread(VsyncThreadProc);
}
}
static void StopVsyncDriver() {
if (g_vsyncRunning.exchange(false)) {
if (g_vsyncThread.joinable()) g_vsyncThread.join();
}
}
#endif // WEBVIEW_CEF_GPU_TEXTURE
static constexpr UINT_PTR kImeSubclassId = 1;
// JJ_MKVMaker: the CEF runtime is downloaded only when the user picks the built-in Chrome
// engine (<exe dir>\cef\libcef.dll). Without it, do nothing that affects the rest of the app
// (no vsync thread, no IME subclass).
static bool CefRuntimePresent() {
wchar_t buf[MAX_PATH * 4];
const DWORD n = GetModuleFileNameW(nullptr, buf, static_cast<DWORD>(sizeof(buf) / sizeof(buf[0])));
std::wstring dir(buf, n);
dir = dir.substr(0, dir.find_last_of(L"\\/"));
return GetFileAttributesW((dir + L"\\cef\\libcef.dll").c_str()) != INVALID_FILE_ATTRIBUTES;
}
// The OS IME must be intercepted in the window procedure BEFORE DefWindowProc
// runs, otherwise the default handling consumes/converts the result string and
// shows its own composition window. We can't do that from the runner's
// post-DispatchMessage hook, so the Flutter view HWND is subclassed here.
static LRESULT CALLBACK ImeSubclassProc(HWND hwnd, UINT message, WPARAM wparam,
LPARAM lparam, UINT_PTR, DWORD_PTR) {
// JJ_MKVMaker: only take over IME while an editable node inside a CEF browser has focus.
// Otherwise (Flutter's own text fields) leave IME to Flutter, so Korean / Japanese
// composition keeps working everywhere else in the app.
{
auto it = webviewPlugins.find(hwnd);
const bool cefEditing = it != webviewPlugins.end() && it->second->isEditableFocused();
if (!cefEditing &&
(message == WM_IME_SETCONTEXT || message == WM_IME_STARTCOMPOSITION ||
message == WM_IME_COMPOSITION || message == WM_IME_ENDCOMPOSITION)) {
return DefSubclassProc(hwnd, message, wparam, lparam);
}
}
switch (message) {
case WM_IME_SETCONTEXT:
// Don't let the OS draw its own composition window; the preedit is
// rendered inside the web page via ImeSetComposition.
lparam &= ~ISC_SHOWUICOMPOSITIONWINDOW;
return DefSubclassProc(hwnd, message, wparam, lparam);
case WM_IME_STARTCOMPOSITION:
// Suppress default composition UI; we drive composition ourselves.
return 0;
case WM_IME_COMPOSITION: {
auto pit = webviewPlugins.find(hwnd);
if (pit == webviewPlugins.end() || !pit->second->isEditableFocused()) {
return DefSubclassProc(hwnd, message, wparam, lparam);
}
HIMC imc = ImmGetContext(hwnd);
if (imc) {
// Committed result -> commit it to the focused browser.
if (lparam & GCS_RESULTSTR) {
LONG bytes = ImmGetCompositionStringW(imc, GCS_RESULTSTR, nullptr, 0);
if (bytes > 0) {
std::wstring ws(bytes / sizeof(wchar_t), L'\0');
ImmGetCompositionStringW(imc, GCS_RESULTSTR, &ws[0], bytes);
pit->second->imeCommitTextNative(ws);
}
}
// Ongoing preedit -> set composition (real-time on-screen text).
if (lparam & GCS_COMPSTR) {
LONG bytes = ImmGetCompositionStringW(imc, GCS_COMPSTR, nullptr, 0);
if (bytes > 0) {
std::wstring ws(bytes / sizeof(wchar_t), L'\0');
ImmGetCompositionStringW(imc, GCS_COMPSTR, &ws[0], bytes);
int cursor = static_cast<int>(
ImmGetCompositionStringW(imc, GCS_CURSORPOS, nullptr, 0));
pit->second->imeSetCompositionNative(ws, cursor);
}
}
ImmReleaseContext(hwnd, imc);
}
// Consume: prevent DefWindowProc from showing the default IME window or
// generating duplicate WM_IME_CHAR/WM_CHAR for the committed text.
return 0;
}
case WM_IME_ENDCOMPOSITION: {
auto pit = webviewPlugins.find(hwnd);
if (pit != webviewPlugins.end()) {
pit->second->imeFinishCompositionNative();
}
return DefSubclassProc(hwnd, message, wparam, lparam);
}
}
return DefSubclassProc(hwnd, message, wparam, lparam);
}
void WebviewCefPlugin::RegisterWithRegistrar(FlutterDesktopPluginRegistrarRef registrar) {
auto plugin = std::make_unique<WebviewCefPlugin>();
plugin->m_textureRegistrar = FlutterDesktopRegistrarGetTextureRegistrar(registrar);
flutter::PluginRegistrarWindows *window_registrar = flutter::PluginRegistrarManager::GetInstance()
->GetRegistrar<flutter::PluginRegistrarWindows>(registrar);
plugin->m_channel =
std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
window_registrar->messenger(), "webview_cef",
&flutter::StandardMethodCodec::GetInstance());
plugin->m_channel->SetMethodCallHandler(
[plugin_pointer = plugin.get()](const auto &call, auto result)
{
plugin_pointer->HandleMethodCall(call, std::move(result));
});
plugin->m_hwnd = FlutterDesktopViewGetHWND(FlutterDesktopPluginRegistrarGetView(registrar));
{
std::lock_guard<std::mutex> lock(g_pluginsMutex);
webviewPlugins.emplace(plugin->m_hwnd, plugin->m_plugin);
}
if (CefRuntimePresent()) {
#ifdef WEBVIEW_CEF_GPU_TEXTURE
// Begin ticking external BeginFrame on every vblank (drives GPU frames).
StartVsyncDriver();
#endif
// Subclass the Flutter view window to intercept WM_IME_* before the engine
// and DefWindowProc handle them (required for correct CJK composition/commit).
SetWindowSubclass(plugin->m_hwnd, ImeSubclassProc, kImeSubclassId, 0);
}
webviewChannels.emplace(plugin->m_hwnd, [plugin_pointer = plugin.get()](std::string method, flutter::EncodableValue* arguments) {
plugin_pointer->m_channel->InvokeMethod(method, std::make_unique<flutter::EncodableValue>(*arguments));
});
plugin->m_plugin->setInvokeMethodFunc([plugin_pointer = plugin.get()](std::string method, WValue* arguments) {
flutter::EncodableValue* methodValue = new flutter::EncodableValue(method);
flutter::EncodableValue* args = new flutter::EncodableValue(encode_wvalue_to_flvalue(arguments));
PostMessage(plugin_pointer->m_hwnd, WM_USER + 1, WPARAM(methodValue), LPARAM(args));
});
plugin->m_plugin->setCreateTextureFunc([plugin_pointer = plugin.get()]() {
#ifdef WEBVIEW_CEF_GPU_TEXTURE
// Zero-copy GPU path: CEF OnAcceleratedPaint shared texture -> Flutter
// D3D11 surface texture. Falls back to the software pixel-buffer path
// only if the D3D11 device could not be created.
auto gpu = std::make_shared<WebviewGpuTextureRenderer>(plugin_pointer->m_textureRegistrar);
if (gpu->isValid()) {
return std::dynamic_pointer_cast<WebviewTexture>(gpu);
}
#endif
std::shared_ptr<WebviewTextureRenderer> renderer = std::make_shared<WebviewTextureRenderer>(plugin_pointer->m_textureRegistrar);
return std::dynamic_pointer_cast<WebviewTexture>(renderer);
});
window_registrar->AddPlugin(std::move(plugin));
}
WebviewCefPlugin::WebviewCefPlugin() {
m_plugin = std::make_shared<WebviewPlugin>();
}
WebviewCefPlugin::~WebviewCefPlugin() {
m_plugin = nullptr;
RemoveWindowSubclass(m_hwnd, ImeSubclassProc, kImeSubclassId);
bool nowEmpty = false;
{
std::lock_guard<std::mutex> lock(g_pluginsMutex);
webviewPlugins.erase(m_hwnd);
nowEmpty = webviewPlugins.empty();
}
webviewChannels.erase(m_hwnd);
if(nowEmpty){
#ifdef WEBVIEW_CEF_GPU_TEXTURE
// Stop the vsync thread before shutting CEF down. Must not hold
// g_pluginsMutex here: the thread takes it while snapshotting.
StopVsyncDriver();
#endif
webview_cef::stopCEF();
}
}
void WebviewCefPlugin::HandleMethodCall(
const flutter::MethodCall<flutter::EncodableValue>& method_call,
std::shared_ptr<flutter::MethodResult<flutter::EncodableValue>> result) {
WValue *encodeArgs = encode_flvalue_to_wvalue(const_cast<flutter::EncodableValue *>(method_call.arguments()));
m_plugin->HandleMethodCall(method_call.method_name(), encodeArgs, [=](int ret, WValue* args){
if (ret > 0){
result->Success(encode_wvalue_to_flvalue(args));
}
else if (ret < 0){
result->Error("error", "error", encode_wvalue_to_flvalue(args));
}
else{
result->NotImplemented();
}
});
webview_value_unref(encodeArgs);
}
void WebviewCefPlugin::handleMessageProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
switch (message) {
case WM_USER + 1:
{
// These were heap-allocated in setInvokeMethodFunc; take ownership and
// free them after dispatch (InvokeMethod copies the arguments).
flutter::EncodableValue *method = (flutter::EncodableValue *)wparam;
flutter::EncodableValue *args = (flutter::EncodableValue *)lparam;
if (webviewPlugins.find(hwnd) != webviewPlugins.end()) {
webviewChannels[hwnd]((*std::get_if<std::string>(method)), args);
}
delete method;
delete args;
break;
}
// WM_IME_* are handled in ImeSubclassProc (before DefWindowProc), not here.
case WM_SYSCHAR:
case WM_SYSKEYDOWN:
case WM_SYSKEYUP:
case WM_KEYDOWN:
case WM_KEYUP:
case WM_CHAR: {
if (webviewPlugins.find(hwnd) != webviewPlugins.end()) {
CefKeyEvent keyEvent = getCefKeyEvent(message, wparam, lparam);
webviewPlugins[hwnd]->sendKeyEvent(keyEvent);
}
break;
}
}
}
} // namespace webview_cef